Integrating Vortex Studio using the SDK - Making a Vortex Application


This section targets developers who want to use the Vortex Application as a part of a bigger application. This section explains what is the VxApplication, how to configure it, load content into it, and get it running from an external application.

Then how to feed Vortex with inputs,  as well as extracting data to feed the non-Vortex application.

VxApplication

 The VxApplication is the central object that contains modules and extensions; At every application update, those modules will read content objects and update them.

There can be only one instance of the VxApplication class per operating system process.
Additionally, in a given process' lifetime, a VxApplication instance should not be destroyed and replaced by a new instance. Proper cleanup isn't guaranteed.

Basics

Modules

VxApplication is initially empty, aside from internal objects it requires to run. Before loading content, it needs modules. Modules will update an aspect of the simulation by updating the extensions associated with it. The most common modules used by a Vortex Application are the Dynamics Module and the Graphics Module. Modules can be added or removed manually or by applying an application setup. Manual management requires the user to create modules using the extension factory and insert them in the application via the API. 

Example: Modules Insertion
...
Vx::VxSmartPtr<VxApplication> application = new VxApplication;
 
auto dynamicsModule = VxSmartInterface<ISimulatorModule>::create(VxDynamicsModuleICD::kFactoryKey);
application->insertModule(dynamicsModule);
...
// Run the simulation
...
application->removeModule(dynamicsModule);
...

The VxApplication API provides methods to insert, remove and enumerate modules. However, it is preferable, to load modules via an application setup file.

For a list of modules provided by Vortex Studio, see Vortex Studio Application Extensions and Modules.

To create your own modules, see Vortex Studio SDK - Customizing Vortex.

Application Extensions

Extensions can also be added directly to the applications. Those extensions are not content but utility objects, such as UI extensions, profiling extensions, display extensions, etc... Like modules, extensions can be added and removed manually or by applying a setup.  The VxApplication API also provides methods to add, remove, enumerate and search extensions. Like modules, it is preferable to load extensions via an application setup file.

For a list of applications extensions provided by Vortex Studio, see Vortex Studio Application Extensions and Modules.

To create your own extensions, see Vortex Studio SDK - Customizing Vortex.

Application Parameters

The application has a few parameters such as frame rate, simulation time, logging, etc... These parameters can be set directly via the API. These parameters are explained below in the section running the application.  If an application setup file is used, applying the setup file will automatically set them to the VxApplication. It is preferable to use a setup file as some parameters in the setup file are not applied to the application but via another API.

Setting up the VxApplication

The Vortex Application can be configured using an ApplicationConfig object. This object, called Application Setup in Vortex Studio, is a convenient way of inserting modules, extensions and setting up parameters for your application as they can be saved into files (.vxc) and reused. The setup files for the Vortex Studio application are located in the folder /resources/config from your Vortex Studio install location. 

Like everything Vortex, that Application setup can be filled in code, although this is not recommended. The Vortex Studio Editor allows the creation and edition of such objects under the Application Setup tab and is the preferred way of making a setup file. 

A setup file is usually used to set up a multi-nodes simulator and set additional options such as profiler settings, additional search paths for plugins and seats. Through this file, you can set some global parameters for the application (such as the default log level, the simulation frame rate, the starting application mode, etc...) but most importantly you can specify which modules and extensions will need to be created along with the application and on which node.

In the Vortex Studio Editor what the SDK calls an ApplicationConfig is referred to as an Application Setup.
Both terms represent the same concept but to avoid any confusion this guide will use the SDK terminology.

Content of the ApplicationConfig

An ApplicationConfig can contain the following elements:

  • Nodes
  • Modules
  • Extensions
  • Parameters
  • Seats

Nodes

A node in a simulator represents, at the OS level, a process that will do some tasks as part of your simulation.
A simulator can be composed of a single node (what we call standalone or desktop) or multiple nodes. In the case of multiple nodes, they may or may not even be on the same physical machine.
Whether they are or not, all nodes will use the Vortex network layer to communicate and exchange data between them.

From the perspective of the ApplicationConfig, a node is simply an ApplicationConfig inside another one. It inherits the elements of the parent ApplicationConfig while also defining new or overriding elements of its own. A node in an ApplicationConfig can thus add modules and extensions only to itself and redefine some of the parameters (such as profiler settings or whether the node is free running or not).

Every Vortex simulator needs a master process. In standalone, since there is only one non-networked process, it is fairly straightforward.
In a network setup, it is a bit more complicated since it is the master node that coordinates the exchange of data with the other nodes, called slaves, and instructs them on what to do.
In most cases, the master will contain the dynamics module; while a slave node never will. If one of your nodes contains the dynamics module it will be designated as the master by the application.

A node can also be explicitly set as a master or a slave by settings its CommunicationModel parameter to Master or slave.

The master node's config (or the config of the application in standalone) almost always contains the DynamicsEngine module. In a multi-node simulator, you must add the NetworkOpenDDS extension to your ApplicationConfig to allow nodes to communicate with each other.

Modules

Modules are a core part of a Vortex application. Modules are not part of the content like the other extensions and objects are. As content is loaded on the application, modules determine which features will be enabled on a given node by taking charge of some extensions.

Extensions

A common workflow is to add extensions to an application by embedding them in a content object and loading the object in the application. Though useful in many cases, if you find yourself adding over and over again extensions to your content that is not directly related to it (for example an extension handling a user interface or network communication), it is a clear sign that the extension probably belongs inside an ApplicationConfig instead. Extensions that are fundamental to your simulator application whatever content is loaded are good candidates for this.

The ApplicationConfig of a distributed simulator must contain a network configuration extension at the root level.

Parameters

A Vortex application exposes some high levels parameters which can be customized from the ApplicationConfig (e.g. frame rate, default log level, etc...). Some of them are global to the whole application while others can be overridden by each node.

Global settings are 

  • Logging Level: Level for logging messages system.
  • Logfile Prefix: Full path prefixed to the log file which will be created by the application. Leaving this field's value empty will resolve to your system's temp folder (%TEMP% on Windows).
  • Log Console: Enable logging messages to the windows console.
  • Simulation Frame Rate: Desired frame rate of the running application.
  • Application Mode: Initial mode of the application at the start (Editing, Simulating, Recording).
  • Global Key Frame Lists Path: Location where all the keyframe lists will be saved.
  • Is Profiler Filtering Enabled: Enable filtering on the performance profiling sections of the application.
  • Profiler Period: Period of the performance profiling sampling in frame number.
  • Profiler Enabled Categories: List of the enabled categories for the application performance profiling.
  • Profiler Disabled Categories: List of the disabled categories for the application performance profiling.
  • Plugin Directories: Additional list of directories used for loading plugins (vxp).
  • Remote Access: Allows to inspect a simulation application from the remote debugger.
  • Python 3: Parameters for the Python 3 interpreter

Node Settings

  • Synchronization Mode: The synchronization mode for this node.
  • Is Profiler Filtering Enabled: Enable filtering on the performance profiling sections of this node.
  • Profiler Period: Period of the performance profiling sampling in frame number for this node.
  • Communication Model: The communication model for the application.
  • Profiler Enabled Categories: List of the enabled categories for the application performance profiling.
  • Profiler Disabled Categories: List of the disabled categories for the application performance profiling.
  • Plugin Directories: Additional list of directories used for loading plugins (vxp).
  • Remote Access: Allows to inspect a simulation application from the remote debugger.

There are more details below about those settings

Seats

The closest analogy to represent a seat is the seat you are sitting on at the simulator. The operator seat on the motion platform, the instructor's chair in another room, all are examples of seats that differ from one another mainly through the hardware that is attached to the seat (screens, motion platform, controllers, etc...). When a role is applied to a seat only the modules of the seat will manage the extensions of the role, otherwise, the extensions will be ignored by the application.

role is a counterpart to the seat. It represents an actor in the simulation and contains extensions that are only added to the simulation when it is applied to a seat.

Let's take for example the development of a tank simulator. The simulator includes room for one student in the operator seat and an instructor in the instructor seat. Inside the simulation, however, there are at least three different roles. One for the instructor (who may be physically inside the simulation as a Vortex Human), one for the tank driver, and another for the tank gunner. The instructor seat is a standard desktop with a mouse and keyboard, perfect to navigate the scene and evaluate what the student does. The operator seat has a motion platform, a steering wheel, pedals, and two joysticks. The driver and gunner roles can alternately be applied to the operator seat which contains all the modules to handle the hardware. However, the gunner role does not contain the extension for the steering wheel and the pedals and inversely the driver role does not contain the joystick extensions. Also, both roles may not use the same displays to reflect a different field of view. On the other side, the instructor seat does not have all the hardware modules of the operator and the instructor extensions may include an extension for using a mouse.

In summary, roles are added to your content directly but seats are part of your ApplicationConfig since they go with the simulator whichever content is loaded on top of it.

Creating an ApplicationConfig

The easiest way to create an ApplicationConfig document is through the Vortex Studio Editor, see Simulator Setup User Guide for more information. In the Editor, you can easily use presets to quickly develop your ApplicationConfig with all the needed components.

While it is preferable to use the Vortex Studio Editor to create a setup file, especially in complex cases such as a distributed simulator, the SDK provides the VxSim::ApplicationConfig class to access those functionalities programmatically. 

The main ApplicationConfig object of an application can contain sub-configs for each node of the application. 

See ApplicationConfig.h for information.

Presets

The class VxSim::ApplicationConfigPreset holds a global list of common ApplicationConfig objects that you can use as a base for your own custom one. A preset add or modify elements of an existing VxSim::ApplicationConfig object.

Applying a preset
VxSim::VxSmartInterface<VxSim::ApplicationConfig> config = VxSim::VxExtensionFactory::create(VxSim::ApplicationConfigICD::kFactoryKey);
std::vector<std::string> availablePresets = VxSim::ApplicationConfigPreset::getAvailablePresets();

...

// Applying a preset to our config object.
VxSim::ApplicationConfigPreset::applyPreset("Master with dynamics", config);

Here's a list of presets that comes with your Vortex Studio installation.

  • ApplicationParameters
  • Master with dynamics
  • Slave with 1 window
  • Slave with 2 windows
  • Slave with no graphics
  • Standalone

Loading a setup file

An ApplicationConfig's main use is to configure a generic application. The application could contain widely different modules and extensions based on the content of its ApplicationConfig.
One such example is the SimApp application that comes with Vortex Studio. If you launch it with the --config <vxcFile> command-line option, it will load and parse the ApplicationConfig given as an argument, modifying the application with the information of the setup document.

Only one ApplicationConfig can be applied on a VxApplication; when it is applied to a VxApplication, the application is wiped of its modules and extensions before applying a new one. It is preferable to do it only once as some settings can only be applied once per application lifetime.

To load an ApplicationConfig object from a file created by the Vortex Studio Editor, a developer have to use a VxObjectSerializer and then extract the loaded setup to apply it on the VxApplication. Applying a configuration on a VxApplication will remove all modules and extensions, before applying your configuration. The ApplicationConfig default parameters values are the same as the default values for an empty VxApplication.

Example:Loading a setup file
...
Vx::VxSmartPtr<VxApplication> application = new VxApplication;
...
VxObjectSerializer serializer;
 
// Load the setup file
if (!serializer.load("../Resources/assets/ExVHLIntegration/ExVHLIntegration.vxc"))
{
        Vx::LogError("Couldn't load application setup in ../Resources/assets/ExVHLIntegration/ExVHLIntegration.vxc.\n");
}

// Extract the ApplicationConfig
VxSmartInterface<ApplicationConfig> config = serializer.getObject();
 
// Apply it
config->apply(application .get());
...

Deploying Your Application

A simple desktop application may be trivial to deploy; create a shortcut that launches the Vortex standard application (also known as SimApp.exe) with the right setup document and parameters (or your own application) and you're done. For something more complex like a distributed simulator, including multiple nodes possibly across the network, deployment is more complex.

Deploying using the Vortex Director

Vortex Studio comes with the Vortex Director, an application that simplifies simulator design. The director is able to launch a distributed simulator in a convenient way.

  1. Starting the Vortex Service

    Make sure the VortexService.exe application is running on each computer that will be involved in the distributed simulator. The Director uses the Service to communicate.
  2. Creating a setup

    Use the Vortex Studio Editor to create your Setup file. The file must contain a network configuration extension at the root level. The extension is only available in the Vortex Editor. It's default values will work with the director.
    1. Setup Deployment

      You must deploy the setup on each node of your simulator. If more than one node is on the same system, you need only one copy of the config for that system. They need to have the same file path.
  3. Creating a simulator

    Use the Vortex Director to create their simulator topology,  associating hardware-based nodes (a process on a networked computer) with your Setup's node. See the Vortex Director documentation for details.
  4. Launching a simulator

    The Vortex Director is also a simulator launcher, it will manage the network configuration of a simulator and will run a network broker to ensure nodes' communication. See the Vortex Director documentation for details.

Deploying your own application using the Vortex Director

A simulation node is usually a Vortex Standard application i.e. Simapp.exe. If you chose to launch your own application using the Vortex Director, you need to specify which nodes will use your application. When the Vortex Director launches your application, it will always send a list of command-line arguments to help you set up your Vortex Application. Simapp.exe knows how to handle these parameters, your application needs to handle them too.

Vortex Command-line Arguments

The arguments' values must be properly set for the Vortex application object to be properly initialized. The same list of arguments is used for the Vortex standard application.

The custom application is launched along with the arguments in the Vortex director the same way it is used using the command line.

The following is an example of the command-line arguments that are used for the Vortex standard application when the player.vxsimulator is launched with the Vortex Director:

SimApp.exe --config "C:\CMlabs\Simulators\distributed.vxc"  --confignode engine  --simulatorname custom --simAppsCount 3  --networkbroker 239.255.44.22:54750 --simulationID 1

Initializing Vortex with the arguments

The VxApplication interface can be easily initialized by your application using the InitializationParameters object.

Initialization Parameters
auto application = VxSim::VxApplication::create();

if (application.valid())
{
    VxSim::InitializationParameters params(argc, argv);
    application->initialize(params);
}

The arguments

The following explains what each argument does.

--config <.vxc file>

The absolute path to Vortex application setup file (.vxc).  The Vortex application object will parse the file and configure itself based on the file's content. For more information, see Simulator Setup User Guide.

The config object is loaded with a VxObjectSerializer.


--confignode <node name>

The node name is defined in the Vortex application setup file. The node's content is applied to the Vortex application object. If not present, only the root node will be used.

The node is fetched from the loaded configuration. After all the other parameters are set to the application, the config node is applied to the application.


--simulatorname <name>

The name of the simulator. It is given to the SimulatorMonitor via setSimulatorName(). 


--simAppsCount <count>

The total number of simulation applications to expect in the simulator. This is important and is used to make sure the master application waits for all applications to be ready before starting any operation.

It is given to the SimulatorMonitor via setApplicationCount()


--networkbroker <IP:Port>

Address and port used by the running applications on each node, to locate each other on the network. The argument has the format: <IP address:Port number> (e.g. 239.255.44.22:54750)

The port number must be in the range (49152, 65535)

If the argument is not specified, the default address and port values are <127.0.0.1:31110>

The argument's value can also be specified via the network extension in the setup file (.vxc). Refer to Network Configuration Extension section of this document for more details.


Before applying the loaded config, the object is parsed to extract the Network extension there should be only 1) and overrides the address, port and simulationId with the one provided.


--simulationID <id>

The unique id is used to identify the network simulation to which this application belongs. The id must be in the range (1, 65535).

The specified value is used to populate the address and port for the communication channels: Event, Kinematic, and Time Synchronization channels.

If the argument is not specified, the default id is 1.

The argument's value can also be specified via the network extension in the setup file (.vxc). Refer to Network Configuration Extension section of this document for more details.

Before applying the loaded config, the object is parsed to extract the Network extension (there should be only 1) and overrides the address, port and simulationId with the one provided.


--datapath <path>

The absolute path to the data provider path of the Vortex application on disk. Setting it makes this running application the Data Provider of the Simulator. For more information, see Vortex Director User Guide under Simulator Data Store Path

It is set after the config is applied to the application.

Deploying manually

If you are developing your own application (not using simapp.exe) for a simulator node, or need to use your own custom deployment, there are some guidelines that must be followed.

OpenDDS and the DCPSInfoRepo

To transfer data and events across a networked simulator, Vortex uses the OpenDDS library. OpenDDS is an open-source implementation of Data Distribution Service for real-time distributed systems.

OpenDDS uses a centralized method of discovering peers. If you are not using the VortexDirector to launch you distributed simulator, it is necessary to run an instance of OpenDDS DCPSInfoRepo server on the network, so that nodes in the distributed simulator can locate each other. Launch the executable located in your Vortex Studio installation folder (e.g. C:\CM Labs\Vortex Studio 2019a\bin\DCPSInfoRepo.exe) before launching your application. 


Creating a network configuration in a setup file

Use the Vortex Editor to create your Setup file. The file must contain a network configuration extension at the root level. The extension is only available in the Vortex Editor. Its default value will work with the director.

Network Configuration Extension

In your application, OpenDDS is represented by the NetworkConfiguration extension which is added at the root of the ApplicationConfig.

The Vortex Studio Director will override some of the parameters of this extension so that you don't need to manage it manually. If you are deploying without the Director you will need to properly set up the network communication.

The extension exposes several configurable parameters as described in the Network Configuration ICD.

Multicast Addresses

All nodes of a given distributed simulator must have the same multicast address and SimulationID. Only the ports should differ for the different channels.

Different distributed simulators must have different multicast addresses and SimulationID.

If different distributed simulators share multicast addresses, your simulators may talk with each other, leading to undefined results.

In order to avoid this issue, there are several options:

  • Using the Vortex Director (recommanded).
  • Making sure that each ApplicationConfig of a given simulator, have different multicast addresses and simulationID.
  • Use the command line option --simulationID of simapp.exe.

Consult these following tips when using that extension

  • Simulation ID: unique ID that enables multiple simulator applications to share the same network. Must be between 1 and 65535. All nodes of the same simulator should share the same simulation ID and each simulator should have a different simulation ID.
    • This parameter can be overridden using command line option --simulationID of simapp.exe. Doing so also overrides the last 2 numbers of the multicast Addresses of the channels and time synchronization. 
  • OpenDDS Info Repository Address And Port : IPv4 address and port (format address:port) of the system hosting the DCPSInfoRepo program. 
    • This parameter can be overridden using command line option --networkbroker of simapp.exe 
  • Event Channel
    • Multicast Address and port: Should be in the form of 239.192.x.x:2. Typically event channel uses port 2. The last 2 numbers and port will be overidden when simapp.exe --simulationID command line option is used. The address must match the Kinematic Channel Multicast Address and Time synchronisation Address.
  • Kinematic Channel
    • Multicast Address and port: Should be in the form of 239.192.x.x:1. Typically kinematic (or data) channel uses port 1. The last 2 numbers and port will be overidden when simapp.exe --simulationID command line option is used. The address must match the Event Channel Multicast Address and Time synchronisation address.
  • Time synchronization
    • ModeDefault is Network time sync
      • Network Time Sync: Synchronization is managed by Vortex. 
      • No Time Sync: There is no synchronization, The slaves will be updated with the latest data package received.
      • External Time Sync: Time is managed by an External Source. In this case, the integrator must manage time and ensure VxApplication::setSimulationTime() is called every step.
    • Address and port: Should be in the form of 239.192.x.x:3. Typically time synchronization uses port 3. The last 2 numbers and port will be overridden when simapp.exe --simulationID command line option is used. The address must match the Event Channel Multicast Address and T the Kinematic Channel Multicast Address.
    • Period: Period (in seconds) at which the slaves are synchronized, the default value is 0.1 seconds.
    • Offset: Offset between time on master and time on slaves (in seconds), the default value is 0.05 seconds. 

Setup Deployment

You must deploy the ApplicationConfig on each node of your simulator. If more than one node is on the same system, you need only one copy of the config for that system. 

Launching Your Application and Applying the Correct Setup

Start your master application and then start each of your slave nodes' applications. Once done, load and apply the config on each of them and then apply the sub-config specific to each node separately. If using the simapp.exe process as a simulator node, use the following command-line options:

  • --config to specify the file path to the setup file
  • --confignode to specify the node name for that process.

Validate Simulator Communication at Startup

It is strongly recommended to validate that all the processes in your simulator are able to communicate with each other before doing any operations. By default, the VortexDirector will make sure all SimApps are able to communicate with each other when it launches a simulator. When deploying a simulator manually, some manual steps must be achieved to ensure this.

Specify the Total Number of Processes in the Simulator

Each "SimApp.exe" process should receive as a command-line argument the total number of expected processes in the simulator being launched. The syntax is the following:

SimApp.exe --simAppsCount <count>

Please note that the total number of processes includes both "SimApp.exe" processes and any custom processes creating their own VxApplication. 


Concerning custom processes creating their own VxApplication, they can specify the total number of expected processes in the simulator by using the application context and the SimulatorMonitor.

Example: Application Count
Vx::VxSmartPtr<VxApplication> application = new VxApplication;
VxSim::ApplicationContext context = application->getContext();
VxSim::SimulatorMonitor* simulatorMonitor = context.getSimulatorMonitor();

simulatorMonitor->setApplicationCount(5); // We expect 5 VxApplications in the simulator

Please note that "setApplicationCount()" MUST be called before the first application update, otherwise it will throw an exception.


Wait for All Processes to be Ready

Now that each VxApplication in the simulation knows how many processes to expect, each process must wait for all processes to be ready before doing any operation.

By default, a SimApp having a "Player Window" extension will show at startup a popup window with a spinning gear until all processes in the simulator are ready. Here is a screenshot of what it looks like:

 


In a custom application creating its own VxApplication, it is the application's responsibility to wait for all processes to be ready in the simulator before doing any operation. Here is an example of how an application can do this:

Example: Waiting to be ready
Vx::VxSmartPtr<VxSim::VxApplication> application = new VxSim::VxApplication;

// Load a Setup document and apply it to the VxApplication
VxSim::VxObjectSerializer serializer;
serializer.load("..\\resources\\config\\test.vxc");
VxSmartInterface setup(serializer.getObject());
setup->apply(application.get());

application->getContext().getSimulatorMonitor()->setApplicationCount(5); // We expect 5 VxApplications in the simulator

while (!application->getContext().getSimulatorMonitor()->areAllApplicationsReady())
{
    Vx::LogWarn("Waiting for simulator to be ready.");
    application->update();
}

Vx::LogWarn("Simulator is ready.");


// Start doing standard operations

Loading content on a deployed simulator

Content is created using the Vortex Studio Editor, but also in code, C++ or python (see section Vortex Studio SDK, - Creating Content). It is possible to load multiples content files and move the object if needed;

Most Dynamics Objects can be moved using the IMobile interface. It is preferable to set the application mode to Editing while loading content and moving some part of it around.

The content you will load must be present on each node of the simulator at the same path. For example, if your application wants to load the scene C:\My Simulator\Assets\Scene\Excavator.vxscene it needs to be present (and all of its sub-objects) with the exact same path on each node.

Before starting the application, content must be loaded into it so that the application will simulate something. 

Loading and unloading using the Simulation File manager

The most convenient way of loading content is to use the VxSimulationFileManager provided by the VxApplication.

Content should be loaded while the simulator is in Editing mode. Some features, such as configuration, cannot be modified while simulating.

The VxSimulationFileManager object will load the content into the application and ensure everything is properly dispatched to the simulator.

Function VxSimulationFileManager::loadObject() is your entry point and takes the following parameters

  • fileName: The filename of the content file, usually a scene or a mechanism.
  • objectName (optional): The name of the object. If none or empty is given, the object will keep the name which it was saved.
  • worldTransform(optional): The position of the object in the world, will be at 0,0,0 if not set. This parameter has no effect on non-IMobile objects.

The simulation file manager can also be used to remove content, using VxSimulationFileManager::unloadObject()

Example:Loading a scene
...
Vx::VxSmartPtr<VxApplication> application = new VxApplication;
...
// Setup the application
...
// Get he file manager to load content
Vx::VxSmartPtr<VxSim::VxSimulationFileManager> fileManager = application ->getSimulationFileManager();


// Load the file, the object returned is the root object, in this case, a scene. The Reference should be kept to inspect the content during the simulation
VxSim::VxSmartInterface<VxContent::Scene> scene = fileManager->loadObject("../Resources/assets/ExVHLIntegration/Design/ExVHLIntegration.vxscene");
if (!scene.valid())
{
	Vx::LogError("Couldn't load the scene in ../Resources/assets/ExVHLIntegration/Design/ExVHLIntegration.vxscene.\n");
}
...
run the Simulator
...
// Cleanup
fileManager->unloadObject(scene.getObject());
 

Getting the global loading status

When running on a multi-nodes simulator, file management should be handled from a single node. The simulation file manager will distribute commands to load content on all nodes. The content is not actually distributed across the network, each node will load the files from their own disk, thus the content files' path must be the same on every computer.

While content is being loaded, the node performing file management should monitor the loading status and ensure it is consistent across all nodes before performing anything else, such as setting the application mode.

Function VxSimulationFileManager::loadObject() is blocking, meaning the function will not return until the object is loaded. When you get the object back, it means the object is loaded locally, but not necessarily on the whole simulator. In order to know when object are loaded, there are 2 ways to proceed.

  1. Periodically call VxSimulationFileManager::getLoadingStatus() between VxApplication::update()
  2. Register a VxSimulationFileManager::Listener with function VxSimulationFileManager::registerListener(). This interface will call the listener functions when the loading status of a file changes. When you are done, unregister the listener.

While waiting, it is important to continue running the application as the simulator communication relies on VxApplication::update() to update its status.

Once the status is kLoaded for all content files, the simulator is ready to proceed. 

Here are the additional VxSimulationFileManager::eLoadingStatus

  • kUnknown: this asset is unknown to the application.
  • kInProgress: the asset is being loaded or unloaded.
  • kLoaded: the asset has been successfully loaded.
  • kLoadingFailed: the asset could not be loaded.
  • kUnloaded: the asset has been successfully unloaded.

Loading Content at Start-Up

An easy way to make your simulator automatically load content at startup without any additional programming or user interface is to use the Content Loader extension.

See Content Loader for more details.

Running the Application

There are 2 main ways to run a Vortex Application:

  1. Calling function run()
    1. run() blocks the calling thread until the application stops. 
    2. It can be stopped from another thread by calling sendExitApplicationRequest().
  2. Calling function update() at each step. 
    1. Function run() essentially calls update() in a while loop
    2. update() will sleep so that it takes no less than a time step in execution time unless the application's synchronization mode is set to none or VSync.
    3. update() returns false when the application needs to stop.

Considering that this section is dedicated to integrating Vortex, you should set the synchronization mode to None and call update manually. 

Function update() should be called once per frame, no matter the simulation mode and also when the simulation is paused, as modules require updates no matter the mode or the pause state.

Application Update

An application update consists of a call to VxApplication::update() and what happens outside of it. Note that the frame transition occurs during the update, but not exactly at the beginning.

Below is the sequence of code:

  1. Function VxApplication::Update() is called
  2. Application Requests made outside of VxApplication::Update() are processed
  3. If the application is synchronized, the application waits here to match the frame rate.
  4. Time is updated, the Frame number is updated (frame transition  occurs here)
  5. If the node is a slave, if updates the extensions with slave data
  6. Calling all ISimulatorModule::onPreUpdate()
  7. Calling all ISimulatorModule::onUpdate()
  8. Calling all ISimulatorModule::onPostUpdate()
  9. If the node is the master, extensions' data is gathered and sent to the slaves
  10. Application Requests made during the module phases are processed
  11. Code exits function VxApplication::Update()
  12. User code between calls to update. In a Simapp process, there is nothing done

Thus, a single application update starts at step 4 and ends at step 3.

Application Modes

Application mode is part of the application context and is used by Modules and Extensions to make some decisions. Modules and extensions are aware of what the application is doing and can adapt their behavior. Implementations of ISimulatorModule and IApplicationListener is also informed of the transitions so they can prepare for the next mode.

There are 3 application modes in Vortex :

  • Editing mode: The simulation is not computed at every step. That's when a user usually changes an object's parameters, move objects, add or remove objects, etc...
  • Simulating mode: The simulation is computed at every step, objects parameters should not be modified.  The simulation can be paused and resumed.
  • Playback mode: The simulation is not computed, objects are moved from the playback source so that content is replayed “graphically”.

The application mode is represented by the enum eApplicationMode. Setting the mode can take more than one update to be set, as some modules may require additional updates before the mode can be changed. The Vortex Studio applications, such as the Editor and the Player, are using those modes. 

The Editor starts in editing mode. The user edits his content. When the user hits the play button, it goes in simulating mode. On stops, it goes back to editing. 

The Player also starts in editing mode. The user selects his content. When the user hits the play button, it goes in simulating mode. When the simulation is stopped, the mode goes back to editing. If the user loads a recording, the player goes into playback mode.

The default mode for a Vortex Application is simulating, it can be set to editing in the Application Setup.

Pausing the simulation

If the mode is set to simulation, the user can pause the simulation by calling pause().

Simulation can be resumed by calling resume().

Synchronization Modes

Synchronization mode indicates the application how to synchronize the simulation time with the clock time. The synchronization mode is represented by the enum eSynchronizationMode. Setting the mode via the VxApplication will distribute de mode among all nodes in the simulator. It is asynchronous can take more than one update to be set. 

There are 4 modes available: 

  • No synchronization (kSyncNone): All nodes go as fast as possible, the simulation frame rate is ignored.
  • Vertical synchronization (kSyncVSync): the application is synchronized with the monitor's refresh rate. VSync must be activated on the graphics card. VxApplication without a graphics module is synchronized as if there was no synchronization.
  • Software synchronization (kSyncSoftware): Synchronization is done by software. The VxApplication will wait between updates to respect its frame rate.
  • Software and vertical synchronization (kSyncSoftwareAndVSync): Synchronization is done by vertical synchronization or by software as needed. This is the default mode.


In the case where the VxApplication is integrated into a custom application (rather than simapp.exe) that controls the update rate and the wait time, the free-running mode should be set to true: Vortex does not sleep during an update to match the frame rate. 

Application Context

The ApplicationContext class contains basic information about the ongoing simulation as well as giving you direct access to certain high-level services.

See ApplicationContext.h for more information.


You can get the ApplicationContext through the VxApplication and through your extensions.

  • VxApplication::getContext()
  • IExtension::getApplicationContext()
  • VxExtension::getApplicationContext()

The ApplicationContext returned from the VxApplication is, of course, always updated by the application. However, the object returned from an extension might or might not be updated by the application; it depends on whether or not the extension you got it from was added to the application. To know whether the ApplicationContext object you hold will be updated by the VxApplication, use the method ApplicationContext::isInApplication(). The ApplicationContext is valid between calls to callbacks IExtension::onActive() to IExtension::onInactive().

Here's a code sample demonstrating some simple uses of the ApplicationContext:

Is the ApplicationContext in the VxApplication
class SelfDrivingTruck : public VxSim::IExtension, public VxSim::IDynamics
{
public:
    SelfDrivingTruck(VxSim::VxExtension* proxy, int version)
        : VxSim::IExtension(proxy, version)
        , VxSim::IDynamics(proxy)
        , inputMaxAccelaration(1.0, "Max Acceleration", &proxy->getInputContainer())
		, mEventListener()
	{
    }

    ~SelfDrivingTruck()
    {
    }

    // From IExtension
    virtual void onActive() override
    {
		mEventListener = getApplicationContext().getEventManager()->registerListener(kEventId, [](const VxSim::EventData& data) {});
    }
	
	// From IExtension
	virtual void onInactive() override
    {
		mEventListener.unregister();
	}

    // From IDynamics
    virtual void preStep() override
    {
        ...
        // If the simulation frame rate is changed through VxApplication::setSimulationFrameRate(),
        // the ApplicationContext will be updated (the time step being the inverse of the frame rate) and we will get the new value
        // through the context the next time this is called.
        double velocityIncrement = inputMaxAccelaration * getApplicationContext().getSimulationTimeStep();
        ...
    }

    VxData::Field<double> inputMaxAccelaration;

private:
	EventListener mEventListener;
};

Application Context Usage

The ApplicationContext is separated into 3 main sections:

  1. The Application simulation information:
    1. isInApplication() - Indicates if the object accessing the context is in the application.
    2. getSimulationFrameRate() - The simulation frame rate, typically 60 fps
    3. getSimulationTimeStep() - The time steps in seconds i.e. 1/frame rate
    4. getSimulationTime() - The current simulation time. Simulation Time is only increased when the simulation is running
    5. getCycleTime() - The current cycle time. The time it took between the last tick to the simulation time. This time should be very close to a time step.
    6. getWaitTime() - The time the application waited before processing the current update. To maintain a constant frame rate, the application will wait if your last update took less than a time step.
    7. getApplicationUpdateTime() - Time it took to process function VxApplication::update(). It includes the wait time.
    8. getFrame() - The current frame. Simulation Frame  is only increased when the simulation is running
    9. getApplicationMode() - Current mode (see 3486827752)
    10. getSyncMode() - The the sync mode of this node
    11. getNodeName() - The node name of this application, as defined in the application setup.
    12. isPaused() - Indicates if the application is in mode simulating and paused.
    13. isSimulationRunning() - Indicates if the application is in mode simulating and running.

  2. The Application Services
    1. getErrorManager() - Get the error management services. Allows to set the log level, register a callback to get errors reported by any nodes.
    2. getEventManager() - Get the event manager, allowing users to send and receive events. (see Events)
    3. getControlPresetsManager() - Get the control presets manager. This is an asynchronous API that allows creation and edition of device mapping. (see Devices)
    4. getProfiler() - Get the profiling services for advanced debugging. (see tutorial ExDynamicsExtension for an example)
    5. getMetricsManager() - Get the Metrics Manager.
    6. getKeyFrameManager() - Get the Key frame manager. (see Key Frames)
    7. getKinematicRecorder() - Get the kinematic recording facilities. (see Record and Playback)
    8. getSimulatorMonitor() - Get the simulator monitor. It is an API that gives you information on the simulator
    9. getRoleSeatManager() - Get the API to manipulate role and seats.
    10. getRemoteAccessManager() - Get the Remote Access settings (see Remote Debugging)
    11. getPython3InterpreterDirectory() - Get the Python 3 interpreter directory
  3. Threading services
    1. getSimulationThreadID() - Return the thread Id of the simulation thread.
    2. isProcessingUpdate() - Indicates if the simulation thread is in the process of calling VxApplication::update().
    3. executeLater() - Pass a function to be executed during the simulation thread. The given function will be called at a safe moment during a VxApplication::update().

Additional Settings

Logging

Log level

The log level can be set per node to the following values, be severity:

  • Off - nothing will be logged
  • Fatal
  • Error
  • Warn
  • Info
  • Debug
  • Trace
  • All - everything will be logged

Log File Prefix

Generates a log file named "prefix"_<hostname>_<process id>. When the value is empty, prefix is the name of the application and the file is located in the %temp% folder. The filename generated can be obtained from VxApplication::getLogFilename().

Profiling

The profiler settings are accessible via the Profiler API that can be obtained from the application context.

See Profiler.h for more information about the API.

See The Profiler Tab for some examples.

Plugin Path

Add additional folders for custom plugin files (vxp) that contain modules and extensions made by a third party. This allows Vortex to find them when loading your simulator Application.

See Customizing Vortex for information about this setting.

Remote Access

Enables remote access so that the Remote Debugger can connect to your Application. The settings are accessible via the RemoteAccessManager API that can be obtained from the application context.

See RemoteAccessManager.h for more information.

See Vortex Studio Debugger for information about the Remote debugger.

Python 3

The python 3 options must be set via the application context. Function setPython3InterpreterDirectory can be used to set the directory. It can only be set once per application, including when using an application setup file. If you use an application setup file and you want to override its interpreter directory, the path must be set before it is loaded.

Note that the shared modules and additional paths can only be set via the application setup.

See Writing Python 3 Applications using Vortex Studio for more details.

Vortex Integration

When integrating Vortex, what a user usually wants to do is to feed Vortex via its inputs, execute an update, then read back the outputs.

So the first thing to do is to get a reference to objects that are required to be driven and objects to read data from.

The are several ways to access objects from content. As shown in the code snippet above, when loading content, the user has access to the root object from the simulation file manager.

From that object, users can use the specific interface or the generic VxObject API to navigate the object hierarchy. To simplify navigation, a user should expose the data it wants to use via a VxVHLInterface at the scene or mechanism level.


Example - Loading content and getting the interfaces
const std::string kPaddle("Paddle");
const std::string kBall("Ball");
const std::string kInterface("Interface");
...
Vx::VxSmartPtr<VxApplication> application = new VxApplication;
...
// Setup the application
...
// Get he file manager to load content
Vx::VxSmartPtr<VxSim::VxSimulationFileManager> fileManager = application ->getSimulationFileManager();


// Load the file, the object returned is the root object, in this case, a scene. The Reference should be kept to inspect the content during the simulation
VxSim::VxSmartInterface<VxContent::Scene> scene = fileManager->loadObject("../Resources/assets/ExVHLIntegration/Design/ExVHLIntegration.vxscene");
if (!scene.valid())
{
	Vx::LogError("Couldn't load the scene in ../Resources/assets/ExVHLIntegration/Design/ExVHLIntegration.vxscene.\n");
}

// Save the mechanism references for easier manipulation and rendering.
auto mechanisms = scene->getMechanisms();
for (size_t i = 0; i < mechanisms.size(); ++i)
{
    auto mech = mechanisms[i];
    if (mech->getName() == kBall)
    {
    	mBall = mech.getObject()->findInterface(kInterface);
    }
    else if (mech->getName() == kPaddle)
    {
        mPaddle = mech.getObject()->findInterface(kInterface);
    }
}
...


Tutorials ExContentParsing shows how to use the Interface of Scenes, Mechanisms, Assemblies, and Parts to navigate content.

Alternatively, a developer can use the IObject interface to search content under it. To search across all content, use the root object (such as the scene). The IObject interface offers some search functions to search by name (findExtensionByName()), by factory key (findExtensionByKey()) or by Interface (findExtensionByType()), recursively or not. 

VxApplication has functions with similar behavior,  templated function findExtensions() and getExtensionCount()/getExtension(), but they are limited to extensions that were added directly to the application via the function add(), or extensions added by an application setup. Content can be added directly to the application, that is what the VxSimulationFileManager does, but only the root object will show in queries.

Updating inputs and reading outputs

Once the reference to the required objects has been established, the external application basically needs to update the inputs, execute an update and read the values from the outputs. Data should not be written into outputs. Parameters value should not change during simulation.

Getting data from content
const Vx::VxID kPositionId("Position");
const Vx::VxID kVelocityId("Velocity");

// See Loading content and getting the interface example above
...
Vx::VxSmartPtr<VxApplication> application = new VxApplication;
...
// Setup the application
...
// Load content a keep reference
...
 
// During the external application loop
... 
// Read input to feed to vortex
...
// Access the input Velocity from the only VHL Interface of the paddle mechanism and moves it at a speed of 4 unit per second in the given direction.
mPaddle->getInput(kVelocityId)->setValue(4. * double(direction));

// Updates the application's modules. In our case it only steps the dynamics engine.
if (!application ->update())
{
    mRunning = false; // The simulation was terminated, we're done.
}
    
// Get the ball mechanism position that by its interface Position output value.
Vx::VxVector3 pos = VxMath::Transformation::getTranslation( mBall->getOutput(kPositionId)->getValue<VxMath::Matrix44>() );
 
// Update external app
 


Vortex Remote

Client setup

The currently supported language and API for the client remotely querying the Vortex interfaces is Python with Google Protocol Buffers and Google RPC (gRPC). Users do not need to be well versed in gRPC to use Vortex Remote, but they should be familiar with Python. Users familiar with gRPC will be able to implement their client in other languages using Vortex's proto files. Advanced users can find Vortex's proto files under the Vortex Studio install location resources\proto\vortex folder.

To set up a Python client for querying the Vortex interfaces, users must first install the "vortex_remote" python package. It can be found under the Vortex Studio install location resources\pip_wheels folder as file vortex_remote-2022.4.0.0-py3-none-any.whl. To start the installation, open a command prompt at your Vortex Studio root folder and type the following command. 

Installation command
python -m pip install "resources\pip_wheels\vortex_remote-2022.4.0.0-py3-none-any.whl"

Listing the available interfaces

Let's use a Demo Scene from the "Vortex Studio Demo Scenes 2022.4" package as a starting point. Specifically "Scenario/Forklift Scene/Forklift.vxscene". Create a python script and copy the following code snippet. In the script, the channel "localhost:10023" needs to be replaced based on your current setup. Please refer to How To Enable Remote Access for an Application if you haven't enabled remote access already. The IP address of the script needs to reflect the IP address of the machine where the node is running. If you are using your own workplace, you can keep localhost. The port number needs to be the same as the one specified in the Listening Port's field of the running node. Please note that it must be different from zero.

Print the list of interfaces
import sys
import grpc

import vortex.types_pb2
import vortex.remote_pb2
import vortex.remote_pb2_grpc
 
if __name__ == '__main__':
  with grpc.insecure_channel('localhost:10023') as channel:
    stub = vortex.remote_pb2_grpc.InterfaceServiceStub(channel)
    interfaces = stub.List(vortex.types_pb2.Empty())
    print(str(interfaces))

Run your python script from your favorite IDE, or from the command line.

Successful output

content_path: "vx://Forklift Scene/Warehouse/Warehouse/Interface"
content_path: "vx://Forklift Scene/Forklift/Gallery/Interface"
content_path: "vx://Forklift Scene/Forklift/Interface"

A failure like the following typically indicates that the python wheel was not installed. Review the Client Setup section.

Failure output

Traceback (most recent call last):
File "Scripts\test.py", line 4, in <module>
import vortex.types_pb2
ModuleNotFoundError: No module named 'vortex'

A failure like the following typically indicates that you used a wrong IP:Port combo. Review the How To Enable Remote Access for an Application page.

Failure output (2)

Traceback (most recent call last):
File "Scripts\test.py", line 11, in <module>
interfaces = stub.List(vortex.types_pb2.Empty())
File "E:\Python3_Test\lib\site-packages\grpc\_channel.py", line 946, in __call__
return _end_unary_response_blocking(state, call, False, None)
File "E:\Python3_Test\lib\site-packages\grpc\_channel.py", line 849, in _end_unary_response_blocking
raise _InactiveRpcError(state)
grpc._channel._InactiveRpcError: <_InactiveRpcError of RPC that terminated with:
status = StatusCode.UNAVAILABLE
details = "failed to connect to all addresses"
debug_error_string = "{"created":"@1649253922.765000000","description":"Failed to pick subchannel","file":"src/core/ext/filters/client_channel/client_channel.cc","file_line":3129,"referenced_errors":[{"created":"@1649253922.765000000","description":"failed to connect to all addresses","file":"src/core/lib/transport/error_utils.cc","file_line":163,"grpc_status":14}]}"

.Proto files as documentation

Refer to "resources\proto\vortex" to see all proto files. For the example above, you can find the API to list interfaces under the "resources\proto\vortex\remote.proto" file. The "List()" call can be found under InterfaceService.

...
message Listing
{
    repeated string content_path = 1;
}

service InterfaceService
{
    rpc Hello(vortex.types.Empty) returns (Greeting) {}
    rpc List(vortex.types.Empty) returns (Listing) {}
    rpc Get(Request) returns (Interface) {}
    rpc Set(Interface) returns (vortex.types.Empty) {}
}
...

In the "Print the list of interfaces" example, "stub" was created from the "service InterfaceService" section, and "stub.List()" was called passing an empty argument. We printed the list of interfaces from the returned "Listing" message.

The "content_path" member in "Listing" is iterable (repeated) like so:

...
interfaces = stub.List(vortex.types_pb2.Empty())
for interface in interfaces.content_path:
  print(interface)


Successful output

vx://Forklift Scene/Warehouse/Warehouse/Interface
vx://Forklift Scene/Forklift/Gallery/Interface
vx://Forklift Scene/Forklift/Interface

Updating inputs and reading outputs

For this section, from "resources\proto\vortex\" refer to "remote.proto" and "types.proto". We'll continue building on the previous "Print the list of interfaces" example. We'll pick the simple forklift interface as an example.

Reading values
...
interface = stub.Get(vortex.remote_pb2.Request(content_path = "vx://Forklift Scene/Forklift/Interface"))
print(interface)

Successful output

path: "vx://Forklift Scene/Forklift/Interface"
input {
name: "Direction"
doubleValue: 0.0
}
input {
name: "Speed"
doubleValue: -0.0
}
input {
name: "Brake"
doubleValue: 0.0
}
...
input {
name: "Speed Lever"
doubleValue: 1.0
}

Use typical python semantics to parse the returned "Interface" message.

Setting values is the mirror operation. Instead of calling "stub.Get()" and receiving an "Interface" message, call "stub.Set()" passing a new or modified "Interface" message. We need to import "types_pb2", create a properly named "vortex.remote_pb2.Interface" and add the new value to give to "Speed Lever".

import sys
import grpc

import vortex.types_pb2
import vortex.remote_pb2
import vortex.remote_pb2_grpc

if __name__ == '__main__':
  with grpc.insecure_channel('localhost:10023') as channel:
    stub = vortex.remote_pb2_grpc.InterfaceServiceStub(channel)
    interface = stub.Get(vortex.remote_pb2.Request(content_path = "vx://Forklift Scene/Forklift/Interface"))
    interface.input.extend([vortex.types_pb2.Field(name = "Speed Lever", double_value = 1.0)])
    stub.Set(interface)

If you execute this script while the simulation is running, you should see the forklift move forward once the new value is set.


Tutorial

See Mechanism Viewer Tutorial for an example of a simple Vortex application