University of illinois Urbana-Champaign

Setting Up, Testing, and Managing a Syzygy Cluster

Cluster Mode

Overview

A Syzygy cluster is comprised of a set of computers connected by a network. Each cluster is controlled by a Syzygy server. There is only one instance of the Syzygy server program, szgserver, per cluster. The group of computers comprising a Syzygy cluster is controlled by an instance of the Syzygy server program, szgserver, which performs several functions:

  • Manages multiple users.
  • Stores a configuration parameter database for each user.
  • Maintains a list of the software components registered with the cluster, with each component being identified by the computer on which it is running, a symbolic string (equivalent to executable name), and a unique ID (equivalent to a Unix process ID).
  • Contains a connection brokering service that allows components that need to connect to automatically find each other.
  • Maintains locks to prevent more than one component at a time from accessing certain cluster resources.
  • Routes messages from one registered software component to another. These messages can be used like Unix signals. For example, messages can cause a component to terminate or to reload its parameters.

Membership in a cluster for a given user is determined by two files:

  • szg.conf, (normally one/computer) which specifies the computer's Syzygy name, describes its network interfaces, and assigns each to a named Syzygy network.
  • szg_<user>.conf (where <user> refers to the operating system user name), which specifies the Syzygy server that OS user <user> is currently logged into.

A Syzygy cluster can be controlled in two ways:

  1. By issuing Syzygy commands from a command shell or from a scripting language such as Python.

     

  2. Programatically using methods of an arSZGClient object, either in C++ or Python. There is almost a one-to-one mapping between the methods of this object and the shell commands

It's not entirely necessary, but it's a lot easier to run cluster applications if each computer in a cluster runs a copy of the remote execution daemon szgd. This allows all of an application's components to be launched by a single command issued from any computer logged into the Syzygy server.

Required Ports: A Note on Firewalls

IMPORTANT: Many operating systems today will install a firewall. The default configuration of this software might very well keep Syzygy from functioning. As a networked system, Syzygy expects to be able to communicate on particular ports. The easiest option is to simply not have a firewall at all. However, this may not be acceptable.

These ports must be allowed through a firewall:

  • For Syzygy to operate in a distributed fashion, you will need to create a cluster. This involves running an instance of the Syzygy server szgserver, which in turn requires specifying a port for incoming TCP connections. The computer on which szgserver runs must allow connections to this port.

     

  • Each computer that is part of the cluster will require a block of ports for Syzygy components to connect to one another. This block defaults to ports 4700-4899, but it can be set manually using the dports command. TCP connections to this block of ports must be let through your firewall.

     

  • Besides this block of ports, UDP port 4620 is used for automated discovery of Syzygy servers. It's not a major problem if this port is unavailable, it just means that you have to use the more specific form of the dlogin command (see  Creating the Login File) and the dhunt command won't find your server.

Reserved Words and Characters

"NULL" or "*" cannot be user-defined user, computer, group, or parameter names. "NULL" internally functions as the indication of an undefined value, while "*" is used in the Syzygy server automated discovery process. The characters

  :  ;  |  /

should also not be used in names.

Creating a Cluster

Creating a Cluster: The Network Configuration File

Each computer in a cluster must contain a network configuration file named "szg.conf". in which network interface data are stored. The default location of this file varies by platform:

  • On Unix-like operating systems (Linux, Mac OS X, Irix), the default location is in /etc/. Note that non-root users cannot write to /etc, so initial network configuration must be done as root if the default location is used.
  • On Windows, the default location is in c:\szg\.

If desired you can set the environment variable SZG_CONF to the path to a directory where this file should be created. Note that all Syzygy users MUST be able to change into the directory containing the szg.conf file.

Every Syzygy program will try to read the szg.conf file, but they generally do not try to write to it. It must exist for programs to work in Cluster Mode but it is not necessary for programs to work in Standalone Mode.

szg.conf is an XML file containing the following information:

  1. The computer's Syzygy name (typically the first part of the DNS name, but it doesn't have to be). Defaults to "NULL".
  2. For each network interface card in the computer there must be a record specifying:
    1. The Syzygy network that this interface is part of. Interfaces connected to the internet should specify "internet"; multiple private networks are allowed and should be assigned distinct names that are meaningful to you.
    2. The interface's IP address and netmask. The netmask defaults to 255.255.255.0.
    3. A "type" value that must currently be set to "IP". Note that there's no default interface record, if you don't add any you simply won't be able to perform any Syzygy networking functions.
  3. A range of ports to be used in connection brokering. This is specified by two numbers, the starting port number and number of ports in the block. The default values are 4700 and 200.

     

    Here is an example of an szg.conf file:
    <computer>
      <name>my_computer</name>
    </computer>
    <interface>
      <type>IP</type>
      <name>internet</name>
      <address>999.999.999.999</address>
    </interface>
    <interface>
      <type>IP</type>
      <name>private_net</name>
      <address>192.168.0.1</address>
    </interface>
    <ports>
      <first>
        4700
      </first>
      <size>
        200
      </size>
    </ports>
    

     

    By default, the szg.conf file is located at c:\szg\szg.conf on Win32 systems and at /etc/szg.conf on Unix systems. An alternate location can be specified by setting the environment variable SZG_CONF to the path of the desired directory (without trailing path delimiter, e.g. /tmp/szg or G:\szg. Remember that to un-set a Syzygy environment variable you can set it to the string "NULL", which Syzygy interprets as "no such variable". Note that this means it is possible to have distinct szg.conf files for different operating system users, should that be desirable.

Creating the Network Configuration File

You don't need to edit szg.conf by hand; command line programs are provided. Each of these commands parse the whole szg.conf file and re-write it from scratch upon completion, minimizing the possibility of error.

To create szg.conf, perform the following steps (to perform these steps you must have write access to the directory where szg.conf will be created):

  1. Login to the computer to be configured.
  2. Assign the computer a Syzygy name. This name must be unique across all the computers in your system. A good choice is the short version of your computer's DNS name (e.g. foobar.isl.uiuc.edu becomes foobar) but other names are possible. We assume that you can write to the file's location. For example, type:
      dname my_computer
    
    Or if you type:
      dname -hostname
    
    it will attempt to set the name automatically to the computer's hostname.
  3. szg.conf must contain information about the network interfaces in the computer, specifically the IP address and netmask. In addition, each interface must be assigned to a Syzygy network. To add an interface to the config file:
      daddinterface <network_name> <address> [<netmask>]
    
    <network_name> assigns the interface to a particular Syzygy network. Internet addresses should use the name "internet". Private networks can use an arbitrary name, but this should be consistent across the private network and different from that assigned to other private networks in the distributed system. For example, the distributed system might contain 2 clusters, each connected internally by a distinct 192.168.0.XXX private network. In this case, the <network_name> associated with each should be different. This lets Syzygy operate properly with respect to connection brokering.

     

    The primary or default network for each computer should be added first. This network should be one to which all computers in the distributed system are connected. Components will expect to connect to the szgserver on this network. Furthermore, if components have a choice about how to connect to one another (i.e. they are connected by several networks), they will default to using the first network.

     

    You can optionally specify a <netmask> for the interface. By default, a netmask of 255.255.255.0 is used.

     

    You can remove networks from the config file using the following command:
      ddelinterface <network_name> <address>
    

     

    Syzygy operates using connection brokering. This means that you will not explicitly assign the ports on which servers will listen for connections. Instead, Syzygy will assign the servers' ports based on a pool it maintains on a per-computer basis. By default, the block of ports numbers 4700-4799 is used, which is likely to be OK on both Unix and Windows machines. However, you can change this block using the following command:
      dports <first_port> <num_ports>
    
    This command allocates a block of ports beginning at <first_port> and containing <num_ports> ports. IMPORTANT: you'll want every port in this block to be free and user services to be able to bind to them. The block should be of reasonable size in order to accomodate all the services that might run on the computer. It is best to be generous when assigning the size. NOTE: some Windows versions do not like user services to bind to ports 5000 and above.

     

  4. After issuing these commands, you can check the stored config file by typing:
      dconfig
    
    The output might look something like this for a computer with two network interface cards:
      computer = foobar
      network = internet, address = 999.999.999.999, netmask=255.255.255.0
      network = private_net, address = 192.168.0.1, netmask=255.255.255.0
      ports = 4700 - 4899
    

Network Configuration File Command Reference

  dname <name>

Sets the computer's name in the szg.conf file. If no ports information is present, it goes ahead and adds a default ports record, reserving the range 4700-4899. New in Syzygy 1.2: If <name> is  -hostname it will attempt to set the name automatically using the gethostname() function (i.e. it will attempt to set the Syzygy name to the hostname). Previously, issuing the dname command without an argument would do this. Now, if issued without an argument it will print the computer's Syzygy name, if defined.

  dconfig

Parses the szg.conf file and prints the information in compact form.

  daddinterface <network_name> <address> [<netmask>]

Adds an interface to szg.conf file with the given name, IP address, and (optionally) netmask. The name should be descriptive (like "internet") and is meant to uniquely identify a network. The netmask can also be given (the default is 255.255.255.0).

 ddelinterface <network_name> <address>
    

Removes the interface with the given name/address pair from the szg.conf file.

  dports <first_port> <num_ports>

Changes the "ports" record in the szg.conf file, with "first_port" giving the number of the first port to be used and "num_ports" giving the number of ports in the block.

Creating a Cluster: Running a Syzygy Server

To run a Syzygy server:

  szgserver <server_name> <server_port>

e.g.

  szgserver my_server 8888

The <server_name> is arbitrary and is the name that users will use for logging in with the dlogin command (see Commandline Programs for Logging Into and Out of a Server). The <server_port> is the TCP port that the server will listen on for connections. All Syzygy components in the cluster will attempt to connect to the server via this port and will maintain that connection throughout their operation, so of course if you're running a firewall this port must be open. Note that this port must not be within the block of ports printed by  dconfig; that block must be available for connection brokering.

You can restrict the IP addresses from which connections will be accepted by appending an optional sequence of whitelist entries, which can either consist of single IP addresses or blocks of IPs in address/netmask format. The latter means that addresses that match the specified address after being AND'ed with the netmask will be allowed to connect. For example, If you typed:

  szgserver my_server 8888 192.168.0.11 130.126.127.0/255.255.255.0

The first whitelist entry, 192.168.0.11, specifies a single address from which connections will be accepted. The second entry specifies that connections from all IP addresses beginning with 130.126.127 will also be accepted.

NOTE: szgserver is meant to run in the background for a long time (like a linux daemon or windows service). Here are some common misunderstandings:

  • Do not run a new copy of szgserver for each application.
  • Do not run a copy of szgserver on every computer in your cluster.

Computer Names According to the Syzygy Server

The Syzygy server does not do name resolution (i.e. does not use DNS). Clients read the network configuration file szg.conf before communicating with the server and transmits the name contained therein to the server.

Creating a Cluster: The Syzygy Login Files

The network configuration file determines the identity of an individual computer in the cluster. This information is insufficient to join a cluster: In order to do that you also need to specify a Syzygy server and user name. This information is maintained in a set of login files, potentially one for each operating system user on the computer. The default locations for these files are:

  • Unix: /tmp/
  • Windows: c:\szg\

An alternate directory can be specified by setting the environment variable SZG_LOGIN to the path to the directory (without a trailing path delimiter). The login file names are of the form szg_<user>.conf, where  <user> refers to the operating system user name.

Each such file contains the following records:

  1. The user's Syzygy user name.
  2. A complete specification of the Syzygy server that the user is currently logged into. This consists of the server's name, IP address and port number.

Here is an example login file:

<login>
  <user>me</user>
  <server_name>my_server</server_name>
  <server_IP>999.999.999.999</server_IP>
  <server_port>
    8888 
  </server_port>
</login>

If the user is not logged into any Syzygy server (e.g. after issuing the dlogout command, necessary for running programs in  Standalone Mode), the login file will look like this:

<login>
  <user>NULL</user>
  <server_name>NULL</server_name>
  <server_IP>NULL</server_IP>
  <server_port>
    0 
  </server_port>
</login>

 

Of course, the fact that this information is stored in a file means that it persists across operating system login sessions; if you log off of the computer and then log in again, you will still be logged into the same Syzygy server.

Creating the Login File

You use the following commands to create and modify the Syzygy login files:

  dlogin <server_name> <syzygy_user_name>
  dlogin <server_IP> <server_port> <user_name>

Two forms of the dlogin command. In the first form you specify the server name and you Syzygy user name and the dlogin program attempts to discover the server by broadcasting a discovery packet on the local network. If that doesn't work (because the current computer can't broadcast to the one running the server), you can use the second form, in which you explicitly specify the server's IP address and port. For example:

  dlogin my_server me

or

  dlogin 127.0.0.1 8888 me

Upon successful dlogin, the information in the login file will be printed, looking something like:

  OS user     = johndoe
  syzygy user = me
  szgserver   = my_server, 999.999.999.999:8888

 

This command associates a Syzygy user name with the current operating system (Windows, Unix) user name. Any subsequent commands issued on that computer while logged in as that operating system user will be interpreted by Syzygy as being issued by the specified Syzygy user. To change Syzygy users for a given OS user you can either (a) dlogin as a different Syzygy user, or (b) dlogout, which disconnects you from any szgserver. As stated above, if you log out of your computer's operating system and then log back in you will still be dlogged in as the same Syzygy user.

Note that the second form of the dlogin command is the one you would use when setting up a mini-cluster on a single computer that is not connected to a network. In this case you should use the localhost or loopback address for the current computer, 127.0.0.1. This address does not support broadcast traffic.

Not also that there is currently no authentication of Syzygy users. Other users that can dlogin to the server can change your Syzygy database parameters or execute your Syzygy applications without restriction.

If both forms of the dlogin command fail: check the following:

  • The networks in the Syzygy config file on the machine on which dlogin was issued may be incorrect. Check them again using dconfig.
  • The Syzygy server tells dlogin to connect using the first address in the szg.conf file on the computer running the server. The machine from which you are trying to dlogin must be able to reach this IP address. If not, use ddelinterface and daddinterface to reorder the addresses in the szg.conf file on the server machine.

To log yourself out of the Syzygy server and reset the login file:

  dlogout

This command resets the fields in the current user's login file to "NULL" or 0 as appropriate. Issue this command before running programs in Standalone Mode.

 dwho
        

Prints the login file of the current operating system user in compact form.

  dhunt

Searches for Syzygy servers. This command reads the szg.conf file and sends a broadcast discovery packet out through each network interface listed in that file. Any servers receiving the packet respond with their names, IP addresses, and port numbers.

Creating a Cluster: The Remote Execution Daemon

The remote execution daemon szgd is a special Syzygy component that executes programs in response to messages (normally sent by users using the dex command). It launched native (i.e. C++) and Python programs, supports multiple users, and manipulates environment variables controlling dynamic linker and python module paths to ensure that loadable objects behave as expected. szgd must connect to an szgserver as it starts, so you must have dlogged in beforehand.

The exec message sent by dex contains the name of current user on the computer on which it was run. For C++ programs, each  szgd queries the Syzygy database (see System Configuration) for the value of the variable SZG_EXEC/path defined for the current Syzygy user and computer. The value of this variable is used as a search path for the specified executable. For Python programs, the variable SZG_PYTHON/executable should point to the python executable and SZG_PYTHON/path is the search path for the user program. See  Syzygy Resource Path Configuration for details.

By convention, dynamic libraries (dlls) are assumed to live in the same directory as the executables that load them. Consequently, when executing a program, szgd prepends the directory in which the program lives to the front of environment variable giving the dynamic linker search path (LD_LIBRARY_PATH on Linux).

The form of the szgd command is as follows:

  szgd <base_paths> [-r]

The required <base_paths> argument should be a semicolon-delimited list of paths to directories or executables; paths to executables should not include the '.exe' suffix on Windows. szgd is in charge of launching programs on each machine in your cluster in response to a remote command. It will only launch programs whose full path begins with one of the paths in the base_paths list (For example, on Windows you probably do not want it to include c:\Windows\). Note that for many command shells the semicolon is the end-of-command delimiter, so you may need to enclose the base_paths argument in quotes if it contains a semicolon. Note also that the comparison of the various path variables to the base_paths argument is currently case-sensitive, even though Windows paths are normally not.

szgd immediately attempts to connect to the szgserver that you are currently dlogged into. By default, it exits on failure to connect. If the optional -r argument is passed it will instead repeatedly attempt to reconnect on failure.

For example:

szgd C:\myapps -r

 

After starting szgd on each computer of your cluster:

  dps

prints something like:

  computer1/szgd/0
  computer2/szgd/1
  computer3/szgd/2
  computer4/szgd/3
  computer5/szgd/4
  computer6/szgd/5

i.e. "computer name"/"process name"/"process ID". There should be a line for each computer on which you have run szgd.

Note that only one instance of szgd can run on each computer; this is ensured by having it attempt to acquire the Syzygy lock <computer>/szgd, where  <computer> is the current computer name. If another szgd already holds this lock the current one quits with an error message.

At this point you have the basic Syzygy infrastructure in place. You need to read about setting up the information in the Syzygy database to specify what components should run on which computers, the configuration of your graphics displays, and so on. See  System Configuration.

The rest of this chapter contains reference material about the distributed operating system.

What is a Virtual Computer?

Every Syzygy application consists of a number of distinct components that may be running on different computers:

  • Administrative components that manage the rest.
  • Rendering components that actually display the virtual world.
  • Input device drivers.
  • Sound output.

virtual computer is the easiest way to specify which of these components will run on which machines in your cluster. Multiple different virtual computers can be defined on the same cluster. There is some support for automatically cleaning up components belonging to a different virtual computer when an application is launched on a new one, but some care is still required to ensure that things work properly when multiple virtual computers are defined.

In the context of a parameter or dbatch file, virtual computer definitions strongly resemble local parameter definitions, i.e. parameter definitions that are specific to a single machine.

A Simple Example

The following is a simple virtual computer definition. This definition must be entered into the Syzygy parameter database via dset or dbatch, see System Configuration. If it is part of a dbatch file, enclose the following in <assign> and </assign> tags:

 vc SZG_CONF virtual true vc SZG_CONF location my_room vc SZG_CONF relaunch_all false vc SZG_TRIGGER map main_computer vc SZG_DISPLAY number_screens 2 vc SZG_DISPLAY0 map render1/SZG_DISPLAY0 vc SZG_DISPLAY0 networks internet vc SZG_DISPLAY1 map render2/SZG_DISPLAY0
    vc SZG_DISPLAY1 networks internet vc SZG_MASTER map SZG_DISPLAY0 vc SZG_INPUT0 map input_computer/inputsimulator vc SZG_INPUT0 networks internet vc SZG_SOUND map sound_computer vc SZG_SOUND networks internet
    

 

Explication of the Example

The virtual computer is named "vc"; Syzygy knows that it is a virtual rather than a real computer from the first line setting SZG_CONF/virtual to true.

The "SZG_CONF/location" parameter is important when multiple virtual computers are defined. Basically, virtual computers with the same location are assumed to overlap, i.e. they both require components to run on some of the same real computers. When an application is launched on a virtual computer, it uses its location to determine if there are running components it can reuse or that are incompatible and must be terminated. The location is used as a key to make sure components running on that virtual computer only connect among themselves and not, for instance, with components running on another virtual computer with a different location parameter in the same cluster. This means that two different virtual computers can share the same set of computers happily with respect to application launching and component reuse if they specify the same location.

Note that if it is not explicitly defined, the location parameter defaults to the name of the virtual computer, in this case "vc".

Known Issue: a drawback to this system is that all users must specify the same location value for it to work properly the first time a new user starts an application, which in turn means that they must learn the correct location value when setting up their parameter files.

"SZG_CONF/relaunch_all" specifies whether each new application should relaunch all of the components that are not actually part of the application itself (e.g. input drivers, sound output, scene graph renderers) or whether it should attempt to use compatible ones that are already running. Usually the default ("false") is fine. However, if for example you're developing a new version of a device driver you might set this to "true" to ensure that your new version always gets launched. If you have overlapping virtual computers defined on the same set of computers, you may also notice errors in the launching of successive applications based on the Distributed Scene Graph Framework (see Programming). There is a known bug that under certain conditions causes szgrender not to quit when a scene graph application is launched on one virtual computer immediately after another scene graph application is launched on a different computer. If you observe this behavior, setting this variable to "true" is a workaround.

The "SZG_TRIGGER/map" parameter specifies where the administrative component should run. This component is actually built into each Syzygy application that uses the provided application frameworks. When an application is launched on a virtual computer, the first instance starts up in "trigger mode". This "trigger instance" scans the cluster and determines which running services are incompatible with the new application. These are terminated. Next, the trigger instance launches needed application components. If it is a distributed scene graph application (see Programming), it also acts as the controller program; if a master/slave application, it launches the master and slave instances. In either case, the trigger stays running until it gets a "quit" message, at which point it shuts down some or all of its components.

The "SZG_DISPLAY" parameters are used to specify rendering displays. First, the value of "SZG_DISPLAY/number_screens" tells how many displays there are in the virtual computer. For each display, two values need to be set. "SZG_DISPLAY#/map" indicates a display on a particular machine. For example, SZG_DISPLAY0/map is set above to render1/SZG_DISPLAY0. This means that virtual computer "vc"'s display #0 gets mapped to display #0 on real computer "render1". Naturally, there must be a machine render1 and it must have an SZG_DISPLAY0 defined (see Graphics Configuration). Second, "SZG_DISPLAY#/networks" specifies which network(s) that display component will use to communicate. In the above example, all components use the public network "internet". Note that there may be more than one display associated with each render machine, in which case multiple rendering components can run on that machine simultaneously if a virtual computer uses more than one of those displays.

"SZG_MASTER/map" designates the display that will run the master instance of the application for a master/slave program, which creates and distributes application state to the slave instances. In the example above, the master instance will run on SZG_DISPLAY0 of machine render1.

"SZG_INPUT#/map" specifies one or more input device drivers to combine into input service # (usually only one, SZG_INPUT0, is used). The user also needs to map an input device to run applications on the virtual computer. The value of SZG_INPUT0/map is of the format:

  computer1/device1/computer2/device2...

If the device name is "inputsimulator" an instance of the Input Simulator will be launched. Otherwise, an instance of DeviceServer will be launched and the device name will be taken to be the name of a global device definition parameter (see Syzygy Input Device Configuration). These devices all get daisy-chained together from right to left, i.e. the last device sends its output to the next to last and so on, with the first device actually communicating with the application. The event indices (see the relevant part of the  Programming chapter) get stacked, e.g. if the first and second device both supply three buttons the first device's will have indices 0-2 and the second will have 3-5 when used in the application.

The value of "SZG_INPUT0/networks" determines the network interface(s) the input devices will use.

"SZG_SOUND/map" specifies the the computer upon which the sound output program SoundRender will run (there's only one sound output program, so the name isn't explicitly specified).

Finally, "SZG_SOUND/networks" gives the network interface(s) that SoundRender will use.

Sequence of Events During Launch on a Virtual Computer

Now we'll examine more closely what happens when an application launches on a virtual computer, paying particular attention to the how it interacts with a previously running application. Again, consider the command:

  dex vc atlantis

 

  1. The dex command first checks to see if "vc" is the definition of a virtual computer by seeing if vc/SZG_CONF/virtual is set to "true". If so, dex determines the trigger machine of the virtual computer via vc/SZG_TRIGGER/map and executes atlantis on that machine, passing the executable a special parameter indicating that it is being used as a trigger to launch the full cluster application.

     

  2. When the application is being used as a launcher, it first makes sure that a previous application isn't already running, by checking, in this case, if the named lock (see Locks) "my_room/SZG_DEMO/app" is held. Here, the "my_room" appearing in the lock name is the virtual computer location. If the lock is held, the trigger sends a kill message to the lock's owner and waits for it to clean up its components and exit. Once the old application has exited, the new application enters a clean-up phase which makes sure appropriate services are running on the cluster and incompatible ones are killed.

     

    1. First, it checks to see if an incompatible render program is running on any of the screens. For virtual computer "vc" and screen 0, this is done by checking whether anyone is currently holding the lock vc1/SZG_DISPLAY0. Note that a render program can remain active on a screen after an application has been killed. For instance, szgrender stays up after a distributed scene graph application has died since it can just accept a new connection from a new distributed scene graph application. If there is a render program currently registered and it is incompatible with the operation of the new render program (szgrender, for instance, cannot display the graphics for a master/slave program or vice-versa), the old one is killed.

       

    2. Next, the application launcher makes sure other required services are running on the cluster, including input devices and sound drivers. In the case of virtual computer "vc", this means making sure inputsimulator is running on computer "smoke" and that SoundRender is running on computer "sound". In each case, if the component is running already, leave it alone. If it isn't running, go ahead and launch it.

       

  3. Now that its environment has been conditioned, the trigger goes ahead and starts the application itself. In the master/slave application case, it launches an application instance for each graphics screen. In the case of a distributed scene graph application, it goes ahead and makes sure szgrender is running on each render node and begins executing the application code locally.

     

  4. Finally, the trigger waits for a kill signal, as might come from a new application, and performs a shutdown procedure upon receiving it. In the distributed scene graph case, this simply means shutting down itself, while, in the master/slave case, this means telling the connected slaves to shut down and waiting for them to do so.

Using Virtual Computers

For information on defining virtual computers, see Virtual Computer Configuration.

To launch an application on a virtual computer, you will need a copy of szgd running on every "mapped" machine, i.e. any real computer whose name appears as the value of a "<something>/map" parameter value in the virtual computer definition. Once this is done, to launch atlantis (a sample application included with the szg source) on virtual computer vc, use the command:

 dex vc atlantis
            

 

You can also launch a single component as part of a virtual computer. This allows you to either launch all the components of a virtual computer by hand one at a time or to re-start a component that has exited for some reason. Type:

 dex <computer> <component_name> [<args>] -szg virtual=<virtual_computer_name>
                

For example, suppose we've had to quit the copy of atlantis running on computer "computer3", which is part of virtual computer "vc". Then we could restart it with:

  dex computer3 atlantis -szg virtual=vc

This also works for services like DeviceServer and SoundRender, see Component Contexts.

There are two ways to kill an application running on the virtual computer. You can directly kill the trigger instance. For that you need to know the location parameter of the virtual computer. For the example virtual computer defined in  Virtual Computer Configuration, that would be "my_room":

  dmsg -c my_room quit

 

Or:

  dkillall vc

 

Here are several useful commands for managing a virtual computer:

  dkillall <virtual_computer>

Kill any application currently running on the given virtual computer, along with any associated services such as szgrender, SoundRender, or DeviceServer.

  dmsg -c <virtual_computer_location> quit

Kills the application, if any, currently running in the given virtual computer location, but leaves services alone.

  restarttracker <virtual_computer>

Restart services associated with virtual_computer (e.g. input devices and sound).

  setdemomode <virtual_computer> [true, false]

If second argument is "true", this sets each render machine to use a fixed head position rather than that reported by the tracking device. The head position is read from the active screen definition (see Graphics Configuration) on each render computer. Each rendering machine will assume that the direction of gaze is perpendicular to its display, and the up direction of the head will be the screen's up direction rotated by the angle (in degrees) given by the screen's fixedheadupangle element. To set things back to normal mode, use "false" as the second argument.

  setstereo <virtual_computer> [true, false]

Turns active stereo rendering on and off for each rendering machine of the cluster.

  screensaver <virtual_computer>

Start szgrender programs on each render machine in the cluster. Since szgrender is black when no application is connected, this is effectively a screensaver.

  calibrationdemo <virtual_computer>

Display a calibration screen useful for alignment and color matching, looking for the calibration picture cubecal.ppm in the SZG_DATA/path of each render machine.

Component Contexts

What is the Component Context?

Every component in Syzygy is executed in a "context". The context determines the overall behavior of the component, whether the component is executing as part of a virtual computer, and what networks it will use for various sorts of communication. It consists of a list of variable/value pairs, as listed below:

  virtual=virtual_computer_name

The name of the virtual computer on which this component is executing. Set to the value "NULL" if it is not executing on a virtual computer (the default).

  mode/default=[trigger, master, component]

The overall behavior of the component. The "trigger" value causes a syzygy application to run as a trigger instance, launching other necessary components on a virtual computer. The "master" value causes a master/slave component to take the master role. The "component" value is a default and has no effect.

  mode/graphics=SZG_DISPLAY(n)

One of SZG_DISPLAY0, SZG_DISPLAY1, etc. If the component opens a graphics window, this determines the screen configuration it will use from among those defined for the computer it is running on. If you specify a display that isn't defined, a default display configuration will be used.

  networks/default=network_list

The value network_list is a slash-delimited list of network names, like internet/my_private_net. It indicates, in descending order of preference, the network across which service connections should occur, assuming that the service type (input, graphics, sound) has not already specified this.

  networks/input=network_list

Defines the network across which input service connections should occur.

  networks/graphics=network_list

Defines the network across which graphics service (szgrender) connections should occur.

  networks/sound=network_list

Defines the network path upon which sound service connections should occur.

  parameter_file=parameter_file_name

If the application is running in Standalone Mode it reads its parameter database from a file in its current working directory. By default it looks for szg_parameters.xml and then szg_parameters.txt, but this can be changed by passing this special parameter.

  user=syzygy_user_name

The Syzygy user under whose auspices the application will run.

  server=IPaddress/port

Identifies the location of the szgserver to which the component should attempt to connect.

Note those last two items in particular: By manipulating them you could run a component as a different user and connecting to a different Syzygy server from the one you are currently dlogged into.

Manipulating Component Contexts

Normally when you launch an application on a virtual computer the application launching process sets up the context appropriately for each launched component. However, sometimes you will want or need to set the context manually; for example, you may need to restart an individual component of an application running on a virtual computer. This can be done in two ways: (1) By setting the SZGCONTEXT environment variable, or (2) by passing special arguments on the command line.

Setting SZGCONTEXT

The value of SZGCONTEXT should consist of a sequence of the above pairs seperated by ';'. You can set this environment variable manually to control the behavior of Syzygy components on a particular computer. A sample one might look something like:

 virtual=vc;mode/graphics=SZG_DISPLAY0;networks/graphics=my_private_net
    

 

Command-line Context Manipulation

To manipulate a context from the command line, pass "variable=value" pairs preceded by "-szg". For example:

  syzygy_executable -szg virtual=vc -szg networks/graphics=internet

 

Obviously, this method is more flexible than setting an environment variable, but it has a weakness: It depends on all Syzygy components (including any applications that you write) processing command-line arguments after the Syzygy client object, arSZGClient, has been initialized (arSZGClient::init()). In framework-based applications (see Programming this happens in the framework object's init() method. When this method is called the special arguments are parsed and removed from the global argv variable, so the application or component won't be faced with them.

Yet another advantage to this method of manipulating the context: It can be used remotely. When you launch an application using the dex command, most context variables specified on the command line are passed on to the component being launched. For example:

  dex render1 szgrender -szg virtual=vc

will launch szgrender on computer "render1" and tell it to behave as a component of virtual computer "vc".

dex will interpret the "user" and "server" arguments, allowing you to launch applications as a different user or even on a different cluster from those specified in the login file. For example:

  dex vc_far_away my_app
         -szg server=faraway_IP_address/faraway_port
         -szg user=somebody_else

 

Connection Brokering and Service Names

Connections to Syzygy services such as input event streams, sound output, and scene-graph rendering are brokered by the Syzygy server szgserver using a service name. Components that provide a service with a particular name will fail to launch if another provider of that same service name is already running, so it is important to understand how services get their names and how to find out what services are offered.

Service names are parially determined by the nature of the service (the "basic service name") and partially by the circumstances under which a component is launched. If a user launches a component, but not in the context of a virtual computer, the component's service name will be:

  basic_service_name/syzygy_user_name

This prevents the actions of one syzygy user from interfering with those of another.

On the other hand, if a user launches a component in the context of a virtual computer, the component's service name will be:

  virtual_computer_location/basic_service_name

(see Virtual Computer Configuration). This allows different users to share application components running in a particular virtual computer location, allowing them to be used as a communal resource.

Base Service Names

Base service names for the various syzygy service are listed below.

  SZG_INPUT#

An input-event service: One of SZG_INPUT0, SZG_INPUT1, etc. The number refers to the driver slot.??? inputsimulator and DeviceServer offer this service. DeviceClient and Syzygy applications in general must connect to it in order to receive input from trackers, joysticks, gamepads, and so on.

  SZG_SOUND

A source of sound information. The sound-playing component SoundRender needs to receive or connect to this service. Syzygy VR framework applications (see Programmingoffer it.

  SZG_SOUND_BARRIER

Synchronization for the SZG_SOUND service. SoundRender connects to this service. Syzygy VR framework applications offer it.

  SZG_GEOMETRY

A source of scene graph information, generally a distributed scene graph framework application (see Programming). The scene-graph rendering component szgrender needs to connect to this service.

  SZG_GEOMETRY_BARRIER

Synchronization for the SZG_GEOMETRY service. szgrender needs to connect to this service. Distributed scene graph applications offer it.

  SZG_MASTER_<application_name>

The master instance of a master/slave application (see Programming) offers data to the slaves via this service. Slave instances try to connect to this service.

  SZG_MASTER_(application_name)_BARRIER

Synchronization for the above service.

Examining Running Services

Examining services can be helful in debugging problems with components not connecting properly.

To list currently-offered services:

  dservices

To list pending service requests, i.e. requests that have not yet connected to a provider of the requested service:

  dpending

 

How it All Fits Together

Consider a brief example.

You launch a distributed scene graph application. It signals the connection broker in the server that it requires an SZG_INPUT service (for user input) and provides SZG_GEOMETRY and SZG_SOUND services (i.e it is a source of virtual-world-object and sound information).

The connection broker notes that there is a running DeviceServer providing SZG_INPUT0, several szgrenders on various computers requesting SZG_GEOMETRY, and a SoundRender requesting SZG_SOUND. In each case, it sends the IP address and port of the service provider to the components requesting the service, allowing them to connect and satisfy their cravings.

Locks

Locks are Syzygy's mechanism for synchronizing access by multiple, networked processes to resources that can only be accessed by one process at a time. They are exposed to the network by the Syzygy server, but they work analogously to mutexes within a multi-threaded program. When a process requires access to a resource, it requests the associated lock; if no other process holds the lock, it immediately acquires it and proceeds to use the resource. If another process is holding the lock, however, the first process is blocked and cannot continue until the second one releases it.

For example, as noted in Virtual Computer Configuration, overlapping virtual computers can be assigned a common "location" parameter. Virtual computers with the same location parameter are assumed to share machines. If one user launches an application on one such virtual computer, you don't want anyone else to be able to start another program on an overlapping virtual computer at the same time.

To prevent this, the first application to launch requests a lock with the name "<virtual_computer_location>/SZG_DEMO/lock". It holds the lock until all of its components have launched, then releases it. Other applications attempting to launch on another virtual computer with the same location parameter will request the same lock and will be blocked until they can acquire it.

Locks are also used to reserve resources. Some Syzygy components are not allowed to run if another instance of the same component is already running on the same computer. When such a component starts, it requests a lock and does not release it until it exits; other instances attempting to start in the meantime will be unable to aquire the lock and will exit. Here are a few components and their named locks:

  • szgd: <computer_name>/szgd.
  • SoundRender: <computer_name>/SoundRender.
  • DeviceServer: <computer_name>/DeviceServer.
  • szgrender: <computer_name>/SZG_DISPLAY# (# is determined by the display it is using, see Graphics Configuration). Multiple szgrenders can run on the same computer if they use different displays.

Finally, locks facilitate automatic shutdown of previously running applications. If a new application requires a particular resource (for example, it needs to run a rendering component on render1/SZG_DISPLAY0), it requests the associated lock. If the request fails, the server sends the Syzygy process ID of the component holding the lock to the application. The new application then sends the offending component a "quit" message and waits for the lock to be released.

To see the existing locks:

  dlocks

 

Command Line Tools Reference

Syzygy also has commands for managing the paramter database (used for holding system configuration data), for sending messages to running components, and for monitoring the status of the system (e.g. finding out what components are running).

Parameter Database Management

The commands for manipulating the Syzygy database are described in the System Configuration chapter.

Interprocess Communications

Syzygy components communicate by sending messages to one another, with the Syzygy serving providing routing information.

Messages have a type and optionally a body. The type is generally a single word, such as "exec" (to launch a component) or "quit" (telling the receiving component to exit). Some message types are only meaningful to certain components.

Of particular interest to applications programmers is the "user" message type. This gets passed on to the application code by means of the application frameworks' user-message callback or method (see Programming). This allows you to control your application by means of messages sent using the dmsg command.

The following commands can be used to send messages to running components:

   dex [-v] <execution_location> <executable_name> [<arguments>]

Launch a Syzygy component or application remotely. <execution_location> should be either

  1. A virtual computer name; or
  2. The Syzygy name of a real computer.

     

    If a virtual computer, an "exec" message is sent to szgd on the virtual computer's "trigger" computer; it in turn sends messages to start other components of the applicatoin. On the other hand, if <execution_location> is a real computer the the message will be sent to szgd on that machine and a single instance of the executable will be launched there.

     

    Most components will return messages to dex regarding their success or failure in launching. By default, only the contents of the final message are printed, but the -v option prints all such messages.

     

    The executable can be either a native (C++) or a Python program. For native programs on Windows hosts, ".EXE" is automatically appended to <executable_name> (so don't add it yourself). Python programs must end in ".py".

     

    Any trailing <arguments> are passed on to the component as command-line arguments.

     

      dex [-v] <executable_name>
    
    A special case, this launches executable_name on the local computer. No additional arguments may be passed to the program.
dkill [-9] [<computer_name>] <executable_label>

Send a "quit" message to the first entry in the Syzygy process table with the specified label (i.e. process name as returned by dps) and (optionally) running on the specified computer. Like unix "kill -9", the -9 option forcibly closes sgzserver's connection to the component. This is what you use if a client has crashed without the Syzygy server realizing that its connection to the client is gone.

The remainder are variants on the dmsg command, which is the most generic message-sending command.

dmsg <ID> <message_type> [<message_body>]

Send a message to the process with the specified <ID> as printed by dps. The message type and optional body are both strings. for example, you might launch an executable by typing (assuming 99 is an szgd ID):

  dmsg 99 exec /home/randomuser/bin/linux

 

dmsg [-r] -p <computer_name> <component_name> <message_type> [<message_body>]

Find the Syzygy component with name <component_name> that is running on computer <computer_name>. Send it a message with type <message_type> and optional body <message_body>. If the -r flag is specified, dmsg requests a reply to its message and waits for that reply, printing it upon receipt.

dmsg [-r] -m <virtual_computer> <message_type> [<message_body>]

Find the component running on the master screen of the specified virtual computer. Send it a message with type <message_type> and optional body <message_body>. If -r, wait for and print reply.

dmsg [-r] -g <virtual_computer> <display_number> <message_type> [<message_body>]

Find the component running on the specified virtual computer's display indexed by <display_number>. Send it a message with type <message_type> and optional body <message_body>. If -r, wait for and print reply.

dmsg [-r] -c <virtual_computer_location> <message_type> [<message_body>]

Find the trigger component for the currently running application in the given virtual computer location (locations are discussed in Virtual Computer Configuration). Send it a message with type <message_type> and optional body <message_body>. If -r, wait for and print reply.

dmsg [-r] -s <service_name> <message_type> [<message_body>]
                

Find the component providing the service <service_name>. Send it a message with type <message_type> and optional body <message_body>. If -r, wait for and print reply.

dmsg [-r] -l <lock_name> <message_type> [<message_body>]

Find the component holding the lock <lock_name>. Send it a message with type <message_type> and optional body <message_body>. If  -r, wait for and print reply.

System Monitoring

dps [<search_tag>]

List all Syzygy processes in the format:

  computer_name/process_name/ID

Adding the <search_tag> parameter lists only those lines containing it.

dtop [d <milliseconds>] | q | t

Unix Only. Repeated dps, like Unix "top" with pretty color coding. Specify the repeat interval as:

  1. "d" followed by a value in milliseconds;
  2. "q" (no delay, cpu hog).
  3. "t" (for stress testing the server, no delay and no display). Hit the "q" key to quit dtop, or kill it using dkill.

     

    dlocks
    
    Prints a list of the locks currently held by Syzygy components, along with their component IDs.

     

    dservices
    
    Prints a list of services currently offered by Syzygy components.

     

    dpending
    
    Prints a list of unfilled service requests. Useful in understanding why something is failing to connect to or otherwise get information from another component.

Troubleshooting

When things go wrong, e.g. an application or application component does not launch or two components refuse to connect, the user needs to understand connection brokering, locks, and the tools to examine them. Misconfiguration or misuse of these features is a major cause of problems when using Syzygy.

  • szgrender, SoundRender, szgd refuse to launch: Each of these components holds a Syzygy lock corresponding to a real world resource during its operation. For szgrender, it is a given chunk of screen real estate. For SoundRender, it is the sound card. For szgd, it is the computer itself. In the case of szgrender, executing a second copy to display on the same virtual screen (SZG_DISPLAY0, SZG_DISPLAY1, etc.) as an already running szgrender will cause the second copy to quit with the following error message (the component ID will vary):
      szgrender error: failed to get screen resource held by component <ID##>.
    
    This is the desired behavior when a resource cannot be shared.
  • A Syzygy VR framework application (see Programming) that is run on a real computer but not as part of a virtual computer fails to launch with the following error message:
      arSyncDataServer error: failed to register service.
    
    Again, this is an expected behavior. The VR framework application wants to offer an SZG_SOUND service. If it is not run as part of a virtual computer, its service name will default to "SZG_SOUND/<syzygy_user_name>". If the same user runs a second framework application on a different computer, it will attempt to provide exactly the same named service. This is not allowed, otherwise SoundRender would not know which of the two to connect to.

     

  • Component A does not connect to Component B even though it should: First, you should run a Connection-Brokering Test. If this tests succeeds, one of the following is almost certainly true:
    • Neither component is being run on a virtual computer, but they are being run with different user names. You might have dlogin'ed with different Syzygy user names on the different computers. In this case, you must be dlogin'ed with the same Syzygy name on both computers.
    • One component is being run on a virtual computer but another is not. Again, this will fail. Both must be running in the same virtual computer location. Recall that every virtual computer has a location; if not specified, it defaults to the virtual computer name.
    • One component is being run in virtual computer location L1 while the other is being run in virtual computer location L2. Again, this is going to fail. The virtual computer locations must be the same for the components to connect.

       

      You can diagnose these problems by running "dservices" to see the names of services which are being offered and by running "dpending" to see the names of unfulfilled service requests. You should see the full name of the offered service, the full name of the request, and see how they fail to match.

       

  • A computer running some Syzygy components crashed, had its power cord yanked, etc. Now these components are stuck in the Syzyg process table, i.e. they still show up int the list of processes printed by dps. Furthermore, new instances of those components cannot be launched. Assume for the sake of example that the relevant component is szgd. The problem occurs because the <computer_name>/szgd lock was never released because szgd did not exit normally. The solution is:
      dkill -9 szgd
    
    (or whatever the actual component name is). This forcibly removes the component name from the Syzygy process table and releases the relevant lock.

Connection-Brokering Test

To verify that basic connection brokering works between computers in your cluster:

  1. Start BarrierServer on one of your cluster machines.

     

  2. Start BarrierClient on another machine. If all is well, the BarrierClient will begin printing the average time necessary to perform each synchronization through the BarrierServer.

     

    Troubleshooting:
    • If BarrierClient fails to start, make sure dlogin succeeded on the machine on which it is running..
    • If BarrierClient starts but does not start printing, you have entered the IP address incorrectly in the szg.conf file on the BarrierServer machine. Use dconfig to examine it and ddelinterface/daddinterface to fix it.

       

  3. Start BarrierClient on other machines.

     

  4. Kill BarrierServer and restart on another machine. If the various running BarrierClients do not reconnect, see the troubleshooting section above.

Testing a Cluster

This section assumes you have installed the szg software (including the sample applications contained in that package) by compiling (both "make" and "make demo"). You will understand the instructions better if you read the Cluster Mode chapter first, but, actually, these two sections can be studied in parallel. This section introduces you to the fault tolerance and flexibility of a Syzygy set-up. Components can appear, disappear, connect, disconnect, and reconnect in any order, which helps during the experimentation and debugging phases of application development.

This tutorial specifically avoids virtual computers in order to encourage freeform experimentation. However, please consider using them in your production environment.

If you run into problems running the tests that cannot be solved by the diagnostics listed, please see Troubleshooting the Distributed Operating System.

A Simple Test

One can run some simple tests without any configuration at all (beyond szg.conf). On any computer in the distributed system, type:

  hspace

 

A window should appear with a green spiderweb on a black background. If the window fails to launch, the only possibility is that the ports were misconfigured on the machine on which it executed. Attempt adjusting the ports block of the computer on which you ran it via dports. The first successfully launched instance of hspace is the "master". Subsequently launched instances will be "slaves", depending upon the master for information about navigation and the state of the world. Go ahead and launch hspace on other computers in the distributed system. You can quit the program by typing ESC in its window.

Next, on any computer in the system, type:

  inputsimulator

 

On that computer, a window will appear with some geometrical objects, the meaning of which is described in Input Simulator. Move the mouse in the resulting window with a button held down. The green spiderwebs should move in unison. If they do not move, the configuration of the computer on which the FIRST instance of hspace (the master) ran must be incorrect. Make sure that on that computer the network addresses are correct in the Syzygy config file. Furthermore, make sure that that computer can communicate with the computer running inputsimulator over one of those addresses.

The inputsimulator program has a server (for input device information) embedded in it. If it fails to launch, attempt adjusting the ports block of the computer on which you ran it via dports. If it launches, but the green lines in the hspace window do not move when the wireframe sphere moves, this is because the input device client embedded in hspace could not connect. Make sure the Syzygy config file on the computer running hspace has correct information and that the computer running inputsimulator is reachable via the first address listed in the Syzygy config file on the computer running hspace. If the latter is false, use daddinterface and ddelinterface to manipulate the config file on the computer running hspace. Then, upon killing and then restarting hspace, everything should work. NOTE: to quit inputsimulator, type ESC in its window.

Note that only one copy of inputsimulator will run at a given time. It offers a service (SZG_INPUT0), and the szgserver enforces that only a single component can offer a particular service. Try running multiple copies of it and observe the failure. On the other hand, try killing inputsimulator and restarting it on another computer in the distributed system. This will work, assuming that the computers in question are configured correctly, as discussed above. The components automatically reconnect and recreate a working application.

Note that when you kill the master instance of hspace, no motion of the inputsimulator will cause the slaves to move. This is because there now exists no master instance. However, the next hspace instance you launch will become the new master and everything will again work.

Database Parameters Example for Confidence Tests

While, as above, some of Syzygy's flavor can be experienced without specific configuration, more interesting effects require it. For instance, reading data files and constructing tiled displays require configuration. Here are some example parameters, in a format readable by the dbatch command. We made the following assumptions in creating this list:

  • Syzygy contains a framework for constructing user applications. In a master/slave application, seperate copies of the application run on each render node, with one application, the master, controlling the execution of the others.
  • In the parameters below, we've assumed that /szg is the location where you unpacked the code. This'll be easy to change to the actual location. Also, the value of the SZG_EXEC/path parameter assumes you are using Linux machines. Pathnames in Windows will use backslashes instead of forward slashes and appropriate drive letters.
  • The parameters used to configure the view are appropriate for a 2x1 tiled wall placed in front of the observer's position in tracked coordinates.
  • The machine running the master program is named "control", while two machines running slaves are named "slave1" and "slave2". These will need to be replaced with the names of your computers, as determined when you set up the computers in your cluster (see  Cluster Mode).

For an explanation of how to get the configuration information into the Syzygy server (using dbatch), please see the System Configuration chapter. However, simply copying the XML from this documentation into a text file and issuing the command "dbatch the_text_file_name" should work.

<szg_config>
<param>
<name>left_side</name>
<value>
<szg_display>
 <szg_window>
   <size width="600" height="600" />
   <position x="50" y="50" />
   <szg_viewport_list viewmode="normal">
     <szg_camera>
       <szg_screen>
         <center x="0" y="0" z="-5" />
         <up x="0" y="1" z="0" />
         <dim width="20" height="10" />
         <normal x="0" y="0" z="-1" />
         <headmounted value="true" />
         <tile tilex="0" numtilesx="2" tiley="0" numtilesy="1" />
       </szg_screen>
     </szg_camera>
   </szg_viewport_list>
 </szg_window>
</szg_display>
</value>
</param>
<param>
<name>right_side</name>
<value>
<szg_display>
 <szg_window>
   <size width="600" height="600" />
   <position x="50" y="50" />
   <szg_viewport_list viewmode="normal">
     <szg_camera>
       <szg_screen>
         <center x="0" y="0" z="-5" />
         <up x="0" y="1" z="0" />
         <dim width="20" height="10" />
         <normal x="0" y="0" z="-1" />
         <headmounted value="true" />
         <tile tilex="1" numtilesx="2" tiley="0" numtilesy="1" />
       </szg_screen>
     </szg_camera>
   </szg_viewport_list>
 </szg_window>
</szg_display>
</value>
</param>
<param>
<name>whole_view</name>
<value>
<szg_display>
 <szg_window>
   <size width="600" height="600" />
   <position x="50" y="50" />
   <szg_viewport_list viewmode="normal">
     <szg_camera>
       <szg_screen>
         <center x="0" y="0" z="-5" />
         <up x="0" y="1" z="0" />
         <dim width="10" height="10" />
         <normal x="0" y="0" z="-1" />
         <headmounted value="true" />
         <tile tilex="1" numtilesx="1" tiley="0" numtilesy="1" />
       </szg_screen>
     </szg_camera>
   </szg_viewport_list>
 </szg_window>
</szg_display>
</value>
</param>
<assign>
slave1 SZG_RENDER texture_path /szg/rsc
slave1 SZG_RENDER text_path /szg/rsc/Text
slave1 SZG_SOUND path /szg/rsc
slave1 SZG_EXEC path /szg/bin/linux
slave1 SZG_DATA path /szg/data
slave1 SZG_DISPLAY0 name left_side
slave2 SZG_RENDER texture_path /szg/rsc
slave2 SZG_RENDER text_path /szg/rsc/Text
slave2 SZG_SOUND path /szg/rsc
slave2 SZG_EXEC path /szg/bin/linux
slave2 SZG_DATA path /szg/data
slave2 SZG_DISPLAY0 name right_side
control SZG_RENDER texture_path /szg/rsc
control SZG_RENDER text_path /szg/rsc/Text
control SZG_SOUND path /szg/rsc
control SZG_EXEC path /szg/bin/linux
control SZG_DATA path /szg/data
control SZG_DISPLAY0 name whole_view
</assign>
</szg_config>

 

Descriptions of parameters:

  • The XML global parameters left_side, right_side, and whole_view are examples of screen configurations. For more information, see System Configuration.

     

  • SZG_RENDER/texture_path specifies base paths to use in locating texture and font data. For example, textures for the atlantis demo are located in szg/rsc/Texture (there's only one, actually, creating the rippling shading on the tops of the sea creatures).

     

  • SZG_EXEC/path is the path to search for executables to be run by the dex command. If "control" runs Windows instead of Linux, you might see something more like this...

     

    control SZG_EXEC path c:\szg\bin\win32
    

     

  • SZG_DATA/path is the path that some executables search for data files. This should be wherever you installed the optional data distribution mentioned above.

You'll have to alter the following for your setup:

  • SZG_RENDER/texture_path should be XXX/szg/rsc (where XXX is the directory in which szg was installed).
  • SZG_EXEC/path should be set to the location of the installed binaries. Look at the discussion of SZGBIN in the chapter on Getting the Software for more information.

The set-up outlined above assumes that the display computers will have monitors side by side. In this example, "slave1" is displaying the left half and "slave2" is displaying the right half. You can easily reverse this by swapping the SZG_DISPLAY0/name parameter values. Or you can set up a completely different type of display by changing the XML of the global parameters left_side and right_side.

WARNING

This file is being re-written. Information in the following section is out-of-date. In particular, the Syzygy Distributed Scene Graph is no longer supported.

Running the Distributed Graphics Confidence Test

These are the basic steps:

  • Configure your system as described in Cluster Mode.
  • Set the database parameters, either one at at time using dset or altogether using dbatch, as in the previous section.
  • Run the main application and the rendering programs as follows (we assume that szgd is running on each of slave1, slave2, and control):
       dex slave1 szgrender
       dex slave2 szgrender
       dex control cosmos
    
  • These commands can be run from any computer in the cluster.

What should happen is that each execution of szgrender causes a black-filled window to open on the appropriate machine. When cosmos runs, each window should show a partial view of a set of rotating, concentric, highly colorful tori, along with a halo of rays that ryhtmically change length.

If you get an error "szgd found no file foo in the SZG_EXEC path ", then you didn't set up the database properly in step 3. The executables in question need to be in SZG_EXEC/path.

The various demo programs, including cosmos, want to connect to a networked input device. See the Input Devices documentation page for an enumeration of the supported devices. For simplicity's sake, here we assume you'll control the demo using the Input Simulator, which translates mouse movements and keyboard presses into tracker-style events.

  dex control inputsimulator

 

Type dps on a member of the cluster and note the output. You can see everything running now. To kill the test:

   dkill control cosmos

 

The szgrender windows will go black again. You can execute cosmos on control again, and the tori will return. Note that you can also run any of these executables from the command line on the individual machines instead of via dex. To kill the other stuff,

   dkill slave1 szgrender
   dkill slave2 szgrender
   dkill control inputsimulator

 

You can also hear sound from many of the demos, assuming you've compiled with fmod support and have a sound card in "control ". Try:

  dex control SoundRender

 

NOTE: the same parameters mentioned above will allow you to run everything on a single box. Typing:

  dex control szgrender
  dex control cosmos
  dex control inputsimulator

 

will bring everything up. Appropriate dkill's will bring everything down.

Running a Master/Slave Application

So far, you've seen how to run a distributed scene graph application. Let's now examine how to run a master/slave application (using dex, dkill, and configured screens). As mentioned above, in a master/slave application, seperate copies of the application run on each render node, with one application, the master, controlling the execution of the others.

We'll use the same three-machine configuration for this example, the difference being that one of the rendering machines, "slave1 " will be running the master application (an unfortunate confusion in names), the other, "slave2 ", will be running the slave application, and "control " will be responsible for input and sound as before.

We can now run a master/slave application, like hspace (one of the included demos) as follows:

   dex slave1 hspace
   dex slave2 hspace

 

To stop the application:

   dkill slave1 hspace
   dkill slave2 hspace

 

To hear sound (assuming you've compiled w/ fmod support and have a sound card in "control "):

   dex control SoundRender

 

You can also run a master/slave application on a single box, just launch all components on, for instance, "control ".

Input Framework

This chapter shows how to capture and process data from input devices, and then covers the internals of Syzygy's input framework.

Overview

Input data in Syzygy comes as three types of arInputEvents.

1. A matrix event, a 4x4 matrix, usually represents the position and orientation (referred to in combination as the placement) of a tracking sensor.

2. An axis event contains a floating-point number representing the state of a scalar input (a slider, or one axis of a joystick).

3. A button event contains an integer, often boolean in meaning (1 or 0).

Within each class, events are identified by a zero-based index. In applications, a particular event type and index is generally associated with a particular function. For example, matrix 0 often represents the placement of the user's head, matrix 1 that of the wand. All event types have a default value reported for exceptional cases like disconnected sensors or out-of-bound indices. These are the identity matrix, 0.0, and 0 respectively.

arInputEvents can be packed into arStructuredData records. These records can be either the native binary format for fast transmission, or ASCII XML for disk storage. Syzygy provides functions for converting between arInputEvents and  arStructuredData, and for reading/writing such XML files.

Input events are organized into trees by input object classes. Input events from devices on separate branches can combine into a single event stream, corresponding to a compound virtual input device. For example, a motion-tracked gamepad can start out as two separate <input_sources> streams of data (from different computers) and then combine into a single stream.

Events can also be transformed, added, or deleted by filters at tree nodes. These filters are usually written in PForth, but C++ offers more flexibility and pain.

In cluster mode, each computer with input hardware runs its own DeviceServer. Each DeviceServer has a service name corresponding to a group of Syzygy database parameters, determined by the device driver it is running. To listen to that device through a particular IP address and port, an application checks this group of database parameters. Here is a list of supported input devices.

New in Syzygy 1.2: Applications running in Standalone Mode can now load device drivers.

The input framework includes standard navigation methods that work with both Syzygy application frameworks.

Finally, there are classes and functions for how the user interacts with objects in the virtual world: grabbing, various kinds of dragging, and hooks for implementing other user-initiated object manipulation.

Examples

The input framework has two programs that produce input events. The first, DeviceServer, wraps device-driver libraries. The second,  inputsimulator, is a GUI emulating a traditional VR tracked wand and head. Both publish their input events on the network for other Syzyzy programs to read. The generic event-reader DeviceClient can listen to either of these two and print out the events it receives.

Running a DeviceServer and testing it with DeviceClient

DeviceServer -s [-netinput] driver_name input_slot [pforth_program]

The flag -s indicates Simple mode: DeviceServer loads only driver_name, rather than a whole pile of device drivers (an "input node configuration".)

The integer input_slot specifies which slot DeviceServer transmits on. In a cluster of PCs, slots are like CB radio channels. Given a slot, at most one DeviceServer transmits on it and arbitrarily many programs (such as DeviceClients) can listen to it.

The string pforth_program is a global parameter whose text is a PForth program used to filter input events from driver_name.

The flag -netinput makes DeviceServer listen for events from the network. on input_slot+1.

DeviceServer [-netinput] node_config_name input_slot [pforth_program_name]

Without "-s driver_name," DeviceServer instead uses the configuration of input nodes defined in the global parameter node_config_name.

  DeviceClient input_slot

Run DeviceClient on the same computer to see the events sent by that DeviceServer. If you pick a different input_slot, of course, the DeviceClient will "tune to a different station" and listen to any DeviceServer transmitting on that slot.

Sending joystick data from one computer to other computers

Plug a joystick or gamepad into a computer. On that computer, dlogin to the cluster and run:

  DeviceServer -s arJoystickDriver 0

DeviceServer runs until you ctrl+C or dkill it. It won't run, though, if another DeviceServer is already transmitting on input slot 0. (In that case, pick a slot different from 0.) From another shell, type:

  DeviceClient 0

DeviceClient prints the raw joystick events. Mashing buttons and twiddling the joystick should make the numbers change. If not, see Troubleshooting.

You can run extra copies of DeviceClient 0 on other computers where you're dlogin'd, too.

Now you can filter the data (remap or disable buttons, scale joysticks) with PForth programs. For example, add this global parameter to a dbatch file (see Syzygy Input Device Configuration).

<param>
  <name> joystick_scaledown </name>
  <value>
    define filter_axis_0
      getCurrentEventAxis 0.000031 * setCurrentEventAxis
    enddef
    define filter_axis_1
      getCurrentEventAxis -0.000031 * setCurrentEventAxis
    enddef
  </value>
</param>

 

Kill the old DeviceServer and run that dbatch file. DeviceClients may keep running. This new DeviceServer scales down two joystick axes by a factor of 32768, and reverses axis 1:

  DeviceServer -s arJoystickDriver 0 joystick_scaledown

 

Configuration of Input Devices

Here is a simple example of an input node configuration for DeviceServer:

  <param>
  <name> idesk_tracker </name>
  <value>
    <szg_device>
      <input_sources> arSpacepadDriver arJoystickDriver </input_sources>
      <input_filters>                                   </input_filters>
      <input_sinks>                                     </input_sinks>
      <pforth>                                          </pforth>
    </szg_device>
  </value>
  </param>

 

The fields <input_sources>, <input_filters>, and <input_sinks> each contain a list of libraries (DLL's or so's) on the SZG_EXEC/path, for example arMotionstarDriver.so. The filename's suffix is omitted, for OS independence. C++ classes defined in these three fields are subclasses of arInputSource, arIOFilter, and arInputSink respectively.

Given this global parameter in a dbatch file, and a computer with a Spacepad and joystick, typing this:

  DeviceServer idesk_tracker 0

 

configures the DeviceServer with the global parameter named idesk_tracker. DeviceServer thus loads the two libraries listed in <input_sources>, and then transmits events from the Spacepad and joystick on slot 0.

At most one DeviceServer runs on one computer. If a computer has several input devices (here, a Spacepad and a joystick), put multiple entries in the input_sources field like this instead of trying to run multiple DeviceServers.

The field <pforth> contains a PForth program to filter data from the input sources, before the data reaches the chain of input_filters and finally the input_sinks. If left empty as here, obviously no special filtering happens.

Transformation to Syzygy Coordinates

For a HowTo about getting your tracker data mapped in to Syzygy coordinates, see Tracker Coordinate Conversion

Supported Input Devices

Background

Input devices usually transmit data to other Syzygy programs through the program DeviceServer, which loads device driver libraries (see Input Devices).

This chapter lists loadable device drivers included in Syzygy. Of course, you can write your own drivers too.

Some devices are configured with extra database parameters. These parameters have a service name corresponding to a parameter group (e.g., SZG_JOYSTICK for joystick devices). They are associated with the computer running DeviceServer.

Device Drivers in the Base Distribution

The following device drivers interface with a single hardware device. For examples with parameters, replace <computer> with the name of the computer running DeviceServer.

Wiimote (experimental)

  • Loadable module: arWiimoteDriver
  • Platform: Linux
  • Service: none

     

    Uses the open-source Wiiuse library (version 0.9 only).
    This driver has a number of different modes: Pointer, Head Tracker, and "Minority-Report-style Finger Tracker", the latter two from ideas by Johnny Chung Lee.
    In Pointer mode for the wiimote it returns 11 button events, 3 axis events corresponding to estimated Euler angles as well as the corresponding rotation (orientation) matrix. If you have the nunchuk plugged in it will return 2 additional buttons, 2 axes for the joystick, 3 axes for the nunchuk Euler angles, and a rotation matrix.
    Exploring the other two modes is left as an exercise for the reader.

5DT Data Glove (new)

  • Loadable module: ar5DTGloveDriver
  • Platform: Windows
  • Service: none

     

    Uses the 5th Dimension Technologies library.
    Returns an axis event corresponding to the scaled value for each joint This is a float between 0 and 1 corresponding to the scaled angle between 0 unbentand 1--the greatest degree of bending since the driver was started. Would probably be better to use the raw values and calibrate the angles manually. Also returns a set of button values based on the built-in gesture or (more accurately, hand pose) recognition; each button event indicates the presence or absence of the corresponding gesture, with only one being true at any given instant.

Joystick

  • Loadable module: arJoystickDriver
  • Platform: Linux, Windows
  • Service: none

     

    For a host with a joystick.
    Linux uses /dev/js0.
    Windows uses the default DirectInput joystick if the driver was built using one of the Microsoft compilers. If built with MinGW g++, it detects all connected joysticks and amalgamates them into a single input device.
    Has 20 axes and 20 buttons.

Intel Wireless Gamepad

Sorry, no longer supported

Ascension MotionStar

  • Loadable module: arMotionstarDriver
  • Platform: All
  • Service: SZG_TRACKER

     

    For a host networked to the base station PC of an Ascension Motionstar Wireless 6DOF Tracker.
    <computer> SZG_TRACKER IPhost XXX.XXX.XXX.XXX
    XXX.XXX.XXX.XXX is the IP address of the Motionstar base station.
    

FaroArm

  • Loadable module: arFaroDriver
  • Platform: All
  • Service: none

     

    Runs on a host RS232'd to a FaroArm mechanical motion tracker.
    Reports one matrix which accounts for the probe dimensions, and two boolean-valued buttons.

Ascension SpacePad

  • Loadable module: arSpacepadDriver
  • Platform: Windows 98
  • Service: SZG_TRACKER

     

    For a host connected to an Ascension SpacePad motion tracker.
<computer> SZG_TRACKER transmitter_offset
  0.97/0.20/0.12/0/-0.05/0.7/-0.72/0/-0.22/0.69/0.69/0/0.09/6.71/-2.72/1

Matrix (16 floats in OpenGL order) by which we post-multiply the device's matrix. Corrects for angled transmitter antenna.

<computer> SZG_TRACKER sensor1_rot 0/0/1/-90

A rotation by which we pre-multiply the device's matrix. Corrects for sensors mounted at an angle. Here, sensor 1 (wand) has been rolled 90 degrees to the left about the vector (0,0,1), i.e., the z-axis.

Ascension Flock of Birds

  • Loadable module: arFOBDriver
  • Platform: Linux/Windows
  • Service: SZG_FOB

     

    For a host RS232'd to the master unit of a Flock of Birds motion tracker. Supports the Flock's transmitters, extended-range transmitters (ERT's), and birds. Calibrated like SpacePad.

     

    <computer> SZG_FOB config 0/3/0/0
    
    A list of codes, one per Flock unit, that configures each unit. Codes are listed in order of their internal Flock IDs. This example shows four units where the first, third, and four units have only a bird, while the second unit has an ERT.
0: Bird
1: Transmitter
2: Transmitter and bird
3: ERT
4: ERT and bird

 

<computer> SZG_FOB com_port 4

Serial port to which the Flock-of-Birds is attached. The number is 1-based on all platforms, which means that e.g. on Linux you need to add one (/dev/xxxxx0 is Syzygy port 1).

<computer> SZG_FOB baud_rate 38400

Baud rate of the Flock, one of 2400, 4800, 9600, 19200, 38400, 57600, 115200.

<computer> SZG_FOB hemisphere lower

Hemisphere of the transmitter in which the birds fly, one of front, rear, upper, lower, left, right.

<computer> SZG_FOB transmitter_offset 1/0/0/0/0/1/0/0/0/0/1/0/0/0/0/1

Matrix by which we post-multiply the device's matrix. Corrects for angled transmitter, just like SpacePad.

<computer> SZG_TRACKER sensor1_rot 0/0/1/90

A rotation by which we pre-multiply the device's matrix, to correct for angled birds. Same as SpacePad.

Ascension Flock of Birds (proprietary)

  • Loadable module: arBirdWinDriver
  • Platform: Windows
  • Service: none

     

    For a host RS232'd to the master unit of a Flock of Birds.
    multiplatform FOB driver is under development.

Motion Analysis EVaRT

  • Loadable module: arEVaRTDriver
  • Platform: Windows
  • Service: SZG_EVART

     

    For a host networked to an EVaRT optical tracker.

     

    <computer> SZG_EVART IPhost XXX.XXX.XXX.XXX
    
    Specifies the dotted-quad IP address of the EVaRT system.

Intersense Tracker

  • Loadable module: arIntersenseDriver
  • Platform: Windows, Linux
  • Service: SZG_INTERSENSE

     

    For any Intersense tracker. Needs Intersense's own DLL (or .so) installed.

     

    <computer> SZG_INTERSENSE sleep 10/0
    
    Sleep for 10 msec after polling the tracker.
    <computer> SZG_INTERSENSE station0_1 0/0/0
    
    The 0th tracker's 1st sensor has 0 buttons and 0 axes. It's probably a head tracker.
    <computer> SZG_INTERSENSE station0_2 4/2/0
    
    The 0th tracker's 2nd sensor has 4 buttons and 2 axes. It's probably a wand.

     

    The SZG_INTERSENSE/convert# parameters for re-mapping the coordinates are no longer used. Use a PForth filter to do the coordinate transformation. See the  Tracker Coordinate Conversion section for more information.

VRPN

  • Loadable module: arVRPNDriver
  • Platform: Linux
  • Service: SZG_VRPN

     

    For a host running a VRPN server that manages VRPN input devices.

     

    <computer> SZG_VRPN name
    
    Name of the VRPN device to connect to.

Serial-port Switch

  • Loadable module: arSerialSwitchDriver
  • Platform: All
  • Service: SZG_SERIALSWITCH

     

    We use this one to get input from a treadmill, but it's fairly general. The idea is that you have a switch. One end of it is connected to the serial port's "Transmit Data" pin and the other to the "Receive Data" pin. The port is opened for reading and writing. A character that you specify is repeatedly written to the port and then a single character is read. If the switch is closed, then the the value read should be the same as the one written. An axis event is generated for each switch transition, with the absolute value of the event being equal to the time in seconds since the preceding event, with the value being negative if it's a 'open' event.

     

    The following database parameters defined on the computer running the DeviceServer affect behavior:
    <computer> SZG_SERIALSWITCH com_port:
        Which port to use. The number is 1-based on all platforms,
        which means that e.g. on Linux you need to add one
        (/dev/xxxxx0 is Syzygy port 1).
    
    <computer> SZG_SERIALSWITCH baud_rate:
        Baud rate of the port, one of 2400, 4800, 9600, 19200, 38400,
        57600, 115200.
    
    <computer> SZG_SERIALSWITCH signal_byte: Character to write,
        defaults to 0x53.
    
    <computer> SZG_SERIALSWITCH event_type:
        What types of switch events to measure, must be one of 'open',
        'closed','both' (default='both').  In all cases the absolute
        value of the transmitted axis event is the time in seconds
        since the last event of the type specified.
    

Record/Playback

  • Loadable module: arFileSource
  • Platform: All
  • Service: none

     

    Plays back a previously recorded event stream, stored in the file inputdump.xml on SZG_DATA/path. Plays at about the same speed as the data file's internal timestamps.

     

    To record such a stream from a DeviceServer, send it the message dumpon. Stop recording with dumpoff. The event stream will be saved in a file inputdump.xml in SZG_DATA/path. This is convenient for elaborate event streams such as full-body motion capture.

Transformation to Syzygy Coordinates

For a HowTo about getting your tracker data mapped in to Syzygy coordinates, see Tracker Coordinate Conversion

Input Device Configuration

This chapter will show you how to configure Syzygy to work with your input devices.

New in Syzygy 1.2: This section is now relevant to running programs in Standalone Mode as well as in Cluster Mode (see  Cluster Mode); applications can now load input device drivers in Standalone Mode, provided the Syzygy libraries were built as dynamic-link libraries.

Input Device Parameters

As discussed in Syzygy System Configuration, a Syzygy parameter or 'dbatch' file contains two types of parameters, local (computer-specific) and  global. Input devices typically have both. You define a global input device parameter that specifies which device driver module(s) to load and any filtering to be performed on the outputs. Then you define local input device parameters on the computer that the device is hooked up to that contain necessary configuration information for each driver module. Finally, you add an input map specification (another local parameter) to a virtual computer definition that makes the device definition a part of the virtual computer.

Global Input-device Parameters

With the exception of the PForth filter field, input device records are simpler than those for  graphics configuration. Each contains four fields:

  • Input Sources: These are the names of the device driver modules to be loaded. The input device configuration allows you to concatenate multiple inputs into a single input device. The available module names are given in the 'Loadable module:' field of the supported input devices list. The names are separate by spaces in the <input_sources> field.
  • Input Sinks: These represent modules that send data to other software that isn't part of the cluster. The only examples so far are the arFileSink for writing data to a file and the arSharedMemSinkDriver for pushing data into an IRIX shared memory segment.
  • Filters: You may specify one or more C++ filter modules to apply to the input data. We have written two of these: the arTrackCalFilter for correcting tracker position data based on a lookup table, and the arConstantHeadFilter substitutes a fixed position for the user's tracked head position. They are basically obsolete, replaced by the PForth filter.
  • PForth Filter: PForth is a small, stack-based language for performing simple operations on Syzyg input data streams, such as rescaling event values or rearranging event IDs. The name stands for Pseudo-Forth, as that's what the syntax is based on. You add a PForth filter to an input device by writing the code directly into the  <pforth> field of the input device record.

     

    As an example, gamepads and joysticks on Linux typically report values between 0 and 64000, whereas on Windows they can report either [-32000,32000] or [0,64000]. Furthermore, the numbering of the buttons and joystick axes may vary between platforms or may simply be inconvenient. Syzygy applications expect a standardized joystick input:
    • axis 0 is a left/right joystick axis;
    • axis 1 is forward/backward;
    • joystick axis values should be between -1 and 1, with (-1,-1) in the lower-left corner.

This sort of standardization is easy to achieve with a PForth filter.

Local Input-device Parameters

Many input devices require additional parameters. For example, devices that connect via serial interface may require e.g.:

  • A serial port number (note: serial ports in Syzygy are 1-based on all platforms).
  • A baud rate.
  • A 'parity' setting.

     

    ...and more. These are specified as local parameters on the computer that the device is hooked up to. The chapter on Supported Input Devices describes the parameters that must be configured to operate any particular device.
Standalone Mode Local Parameter

When you want to load an input device in Standalone Mode, you must set the variable SZG_STANDALONE/input_config in the parameter file to the name of a global input device definition record. Note that currently if you specify an input device the Input Simulator is deactivated.

Virtual Computer Input-device Parameters

Finally, you add an input map specification to a virtual computer definition. This resembles a local parameter; it says that a particular global input device parameter will run on a particular computer in the context of the current virtual computer.

Input Event Streams

Input data in Syzygy comes in the form of a stream of events. There are currently three types of events. Matrix events, which usually represent the position and orientation (referred to in combination as the placement) of a tracking sensor. Axis events contain a floating-point number representing the state of a continuously-varying input. Button events contain an integer, usually representing a binary on-off input device. Within each class, events are identified by a 0-based index; in applications, a particular event type and index is generally associated with a particular function, for example matrix event #0 is generally assumed to represent the position and orientation of the sensor attached to the user's head. All three event types have a default value that will be returned whenever an application requests an event outside the range of existing indices; for matrices this is the identity matrix, for the other two types it is 0.

Transformation to Syzygy Coordinates

For a HowTo about getting your tracker data mapped in to Syzygy coordinates, see Tracker Coordinate Conversion

Troubleshooting

If your program isn't getting input, verify that DeviceServer is running on all computers connected to input devices:

  dps DeviceServer

lists all DeviceServers running on the cluster. If one is missing, check that computer's console for error messages.

If all DeviceServers are running and you're still not getting input events, run the DeviceClient diagnostic program as described in  Syzygy Input Framework: Practical Examples.

Examples

As an example of an input device configuration record, here's the device definition for the wireless gamepad used in the Beckman Institute Cube. This gamepad has a 2-D joystick and 8 buttons. The device configuration record specifies the driver module to be loaded ('arJoystickDriver') and applies a PForth filter to re-scale the values returned by the joystick to the range [-1,1] (axis event #0 represents the left/right position of the joystick, while axis event #1 represents its front/back position):

<param>
  <name> cube_joystick </name>
  <value>
    <szg_device>
      <input_sources> arJoystickDriver </input_sources>
      <input_sinks></input_sinks>
      <input_filters></input_filters>
      <pforth>
        define filter_axis_0
          getCurrentEventAxis 0.000031 * setCurrentEventAxis
        enddef
        define filter_axis_1
          getCurrentEventAxis -0.000031 * setCurrentEventAxis
        enddef
      </pforth>
    </szg_device>
  </value>
</param>

Note that if you combine two input sources in a single record, and they both provide some of the same kinds of events (e.g. both provide button events), then the event indices of the second input source follow those of the first. For example, if in this example we followed arJoystickDriver with another device which had two buttons, then the original gamepad's buttons would still have indices 0-7 but the new one's buttons would be 8 and 9.

Configuration records for tracking devices are generally the most complex. Here's the device definition for the MotionStar Wireless tracker we use in the Cube. The PForth filter aligns the tracker coordinate system with the Cube's structure, uses a lookup-table procedure to correct the distortions in the tracker data, compensates for the different ways the two sensors are mounted on the LCD goggles and on the gamepad, and applies a simple IIR filter to the vertical component of the head position to suppress the rather excessive noise:

<param>
<name> cube_tracker </name>
<value>
  <szg_device>
    <input_sources> arMotionstarDriver </input_sources>
    <input_sinks></input_sinks>
    <input_filters></input_filters>
    <pforth>
      initTrackerCalibration
      initIIRFilter

      /* Define stuff to rotate & remap positional coordinates
         and rotate orientation by 135 degrees around Y */
      matrix yTransRotMatrix
      -45. yaxis yTransRotMatrix rotationMatrix

      vector oldLo
      vector oldHi
      vector newLo
      vector newHi
      1.2 -5. 1.2  oldLo vectorStore
      10.4 5. 10.4 oldHi vectorStore
      -5.  0. -5.  newLo vectorStore
      5.   10. 5.  newHi vectorStore

      vector oldRange
      vector newRange
      oldHi oldLo 3 oldRange arraySubtract
      newHi newLo 3 newRange arraySubtract

      vector newOldRatio
      newRange oldRange 3 newOldRatio arrayDivide

      vector position

      define rescale_position
        position oldLo 3 position arraySubtract
        position newOldRatio 3 position arrayMultiply
        position newLo 3 position arrayAdd
      enddef

      matrix yRotMatrix
      -135 yaxis yRotMatrix rotationMatrix

      matrix inputMatrix
      matrix rotMatrix
      matrix transMatrix

      /* Apply transformations that are common to all sensors */
      define filter_all_matrices
        inputMatrix getCurrentEventMatrix

        /* Extract translation, rotate it by 45 degrees about y */
        inputMatrix position extractTranslation
        yTransRotMatrix position position vectorTransform

        /* Rescale position coords */
        rescale_position

        /* Rotate rotation component about Y */
        inputMatrix rotMatrix extractRotationMatrix
        yRotMatrix rotMatrix rotMatrix matrixMultiply

        /* Recombine translational and rotational components */
        position transMatrix translationMatrixV
        transMatrix rotMatrix inputMatrix matrixMultiply

        /* Apply tracker-calibration lookup table */
        inputMatrix inputMatrix doTrackerCalibration

        inputMatrix setCurrentEventMatrix
      enddef

      /* Fix rotational matrix components
         (depends on how sensors are mounted) */
      matrix headZRotMatrix
      matrix wandZRotMatrix
      matrix wandTweakMatrix

      /* Head sensor is mounted sideways */
      -90 zaxis headZRotMatrix rotationMatrix

      /* Wand sensor is mounted upside-down */
      -180 zaxis wandZRotMatrix rotationMatrix

      /* Wand sensor points upwards by about 20 degrees
         with gamepad held subjectively level (actually
         tilted up slightly, but it feels right) */
      -20. xaxis wandTweakMatrix rotationMatrix

      matrix wandRotMatrix
      wandZRotMatrix wandTweakMatrix wandRotMatrix matrixMultiply

      /* Head-specific filter (applied after generic, above) */
      define filter_matrix_0
        inputMatrix getCurrentEventMatrix
        inputMatrix inputMatrix doIIRFilter
        inputMatrix headZRotMatrix inputMatrix matrixMultiply
        inputMatrix setCurrentEventMatrix
      enddef

      /* Hand-specific filter (applied after generic) */
      define filter_matrix_1
        inputMatrix getCurrentEventMatrix
        inputMatrix wandRotMatrix inputMatrix matrixMultiply
        inputMatrix setCurrentEventMatrix
      enddef

    </pforth>
  </szg_device>
</value>
</param>

PForth Input-filtering Language

Thanks to Jim Crowell for creating the Syzygy PForth support.

PForth Documentation

The modules arPForth and arPForthStandardVocabulary implement the PForth (P for Pseudo) language, which is used by the input filter arPForthFilter. Each instance of DeviceServer contains an arPForthFilter, and so does inputsimulator. The intent is to provide a means of performing simple manipulations on input events based on information in a text file.

PForth is a FORTH-like language. This is, it's stack-based and uses RPN notation like an HP calculator. For example, to add two numbers together you would type 3 2 + ("Place the numbers 3 and 2 on the stack, then call the '+' word, which takes the top two numbers off the stack and pushes their sum onto the stack"). PForth is compiled; when the PForth filter is loaded, the source code gets converted into an STL vector<> of pointers to objects, one for each PForth word. Running a filter word consists in iterating through its vector<> of pointers and calling i->action() for each one, so it's quite fast.

PForth is virtually identical in usage to Forth, except it has a very limited vocabulary geared towards manipulating input events, and some of the less informative words have been changed (for example, "!" and "@" have been renamed "store" and "fetch"). New words can be defined from sequences of existing words using "define" and "enddef", as in a Forth ":"/";" colon-definition, or entirely new actions can be written in C++; see arPForthStandardVocabulary.cpp for examples.

Language Concepts

A PForth program is just a series of words separated by whitespace. The set of words that PForth understands is called the dictionary. These words typically operate on numbers in a stack. Unlike Forth, the basic data type is a floating-point number. There is also a dynamically-allocated data space that can be used for storing variables and matrices (matrix operations generally take place in the data space). Note that the data space starts out with a size of zero, and must be grown using the "variable" and "matrix" commands. Attempting to read or write from unallocated data space will result in an error.

For most purposes, PForth programs are all kept in the user's dbatch file, in a device definition. Here's an example of an event-filtering program that swaps matrices 0 and 1:

<pforth>
  define filter_matrix_0     /* Each time we come across a
                                matrix event with index 0... */
    1 setCurrentEventIndex   /* ...change its index to 1 */
  enddef
  define filter_matrix_1
    0 setCurrentEventIndex
  enddef
</pforth>

 

For a more complicated example, here is a program that scales axes 0 and 1 from the range (-32,000, 32,000) to the range (-1, 1), swapping the polarity of axis 1. It also maps axis 2 to axis 3, changing its range from (0, 64,000) to (-1, 1), with a change in orientation. It maps axis 5 to 2, while changing its range from (0, 64,000) to (-1, 1). Finally, whenever it gets an event on axis 1, it generates a constant 4x4 matrix. This filter is used to have a particular 2 analog stick gamepad emulate a VR controller (to some degree).

<pforth>
  matrix fixedHeadMatrix                   /* Declare a matrix variable */
  0 5 0 fixedHeadMatrix translationMatrix  /* ... and store a +5 y-translation
                                              matrix in it */

  define filter_axis_0
    getCurrentEventAxis 0.000031 * setCurrentEventAxis 
                                           /* rescale axis value */
  enddef
  define filter_axis_1
    fixedHeadMatrix 0 insertMatrixEvent    /* Create new matrix event with
                                              index 0 and value from temp */
    getCurrentEventAxis -0.000031 * setCurrentEventAxis 
                                           /* ...and rescale this axis event */
  enddef
  define filter_axis_2
    getCurrentEventAxis -0.000031 * 1 - setCurrentEventAxis 
                                           /* rescale and center axis value */
    3 setCurrentEventIndex                 /* re-map event to axis #3 */
  enddef
  define filter_axis_3
    4 setCurrentEventIndex                 /* Change event index to 4 */
  enddef
  define filter_axis_5
    getCurrentEventAxis 0.000031 * 1 - setCurrentEventAxis 
                                           /* rescale and center axis value */
    2 setCurrentEventIndex                 /* re-map event to axis #2 */
  enddef
</pforth>

 

Vocabularies

PForth is extendable in two ways: you can define new words inside a PForth program that concatenate existing words, or you can easily add new words at the C++ level if needed. There are currently two defined vocabularies at the C++ level, the standard vocabulary defined in arPForthStandardVocabulary.cpp and the event-filtering vocabulary in arPForthEventVocabulary.cpp.

I'll use the standard Forth notation to indicate the effect each vocabulary word has on the stack. The format is

( <stack contents before execution> -- <stack contents after execution> ).

I'll use x# to represent a floating-point number, n# to represent an integer, and addr to represent an address (an index into the dataspace). A couple of examples:

  1. ( x1 x2 -- x3 ) means that the word needs to pop two numbers off the stack and will push a single number onto the stack on completion. Note that x1 must have been placed on the stack before x2, i.e x2 is on top of the stack.

     

  2. ( addr n1 -- ) means that the word will pop an address (a positive integer) and an integer off the stack and not push anything onto it.

PForth Standard Vocabulary

These are basic math, memory-management, and flow-control words. The first few words actually take effect at compile time. They do not modify the stack or the data space, but they may remove succeeding words from the input stream (the program). This will be indicated by <word> or <words>. They also typically add words to the dictionary. Note that additions to the dictionary are permanent (i.e. last until the arPForth object is destroyed) and attempting to redefine a word in the dictionary will result in an error.

NEW 5/6/05: Added a bunch of words using (3-element) vectors, and several array... words for performing element-by-element operations on arrays.

<number> (a string representing a number, e.g. "123" or "-12.5")

Effect: at compile time, creates a nameless action that will push the number onto the stack, and appends that action to the current program or word definition. In other words, typing a number into your program causes that number to be placed on the stack at the appropriate point in program execution.

variable <name>

Effect: at compile time, allocates two cells in the dataspace and places the value 1 in the first one (indicating a scalar). Then it adds a word <name> to the dictionary that causes the address of the second cell to be pushed onto the stack. This new word then acts as a pointer to the data cell. Example: "variable x 12 x store" causes the number 12 to be placed in the data cell pointed to by "x".

constant <name> <number>

Effect: at compile time, it adds a word <name> to the dictionary that causes the specified number to be pushed onto the stack. Example: "constant x 12 x" causes the number 12 to be placed on the stack.

matrix <name>

Effect: at compile time, allocates 17 cells, places the number 16 in the first one, and installs a new word <name> in the dictionary that pushes the address of the second cell onto the stack.

array <numItems> <name>

Effect: at compile time, allocates <numItems>+1 cells, places the number <numItems> in the first one, and installs a new word <name> in the dictionary that pushes the address of the second cell onto the stack.

define <name> <words> enddef

Effect: at compile time, adds a new word <name> to the dictionary. All succeeding words in the program until "enddef" are compiled into the definition of <name>; those words are executed each time after that <name> is encountered.

if <words> else <words> endif  ( x -- )

Effect: at compile time, creates a nameless action containing two subprograms. Words between "if" and "else" are compiled into the first subprogram, words between "else" and "endif" are compiled into the second.&nbsp; "else" is optional, if omitted there is no second program.&nbsp; At runtime, the top value is popped off the stack; if it is >= 1, the first subprogram is executed; if < 1, the second. (NOTE: let me know if you can think of a reason why the test should work differently, I just took the path of least effort there).

string <name> <words> endstring

Effect: at compile time, allocates a single cell in the separate string dataspace (yes, an entire string is an atomic variable and they live in their own data space) and installs a new word <name> in the dictionary that pushes the address of the cell onto the stack. This is intended to be used with the database vocabulary (which hasn't really gone anywhere yet), e.g. you could get the value of a database parameter and compare it to a string constant.

/* <words> */ (a comment)

Effect: at compile time, discards all words between the /* and */. Remember that the delimiters must be surrounded by whitespace. No runtime effect.


The remaining words have no compile-time effects.

not (x1 -- n1 ) Places a 1 on the stack if x1 < 1.0, a 0 otherwise.

= (x1 x2 -- n1 ) Places 1 on stack if x1 = x2, 0 otherwise.

less (x1 x2 -- n1 ) Places 1 on stack if x1 > x2, 0 otherwise.

greater (x1 x2 -- n1 ) Places 1 on stack if x1 < x2, 0 otherwise.

lessEqual (x1 x2 -- n1 ) Places 1 on stack if x1 >= x2, 0 otherwise.

greaterEqual (x1 x2 -- n1 ) Places 1 on stack if x1 >= x2, 0 otherwise.

KnownBug: Turns out that the version of TinyXML that we use to parse the parameter files does not allow you to embed e.g. '<' in an XML record and does not convert e.g. '&lt;' to '<'. The full-word equivalents of the arithmetic-comparison words were added to work around this problem. Thus, these four words are currently unusable:

> (x1 x2 -- n1 ) Places 1 on stack if x1 > x2, 0 otherwise.

< (x1 x2 -- n1 ) Places 1 on stack if x1 < x2, 0 otherwise.

>= (x1 x2 -- n1 ) Places 1 on stack if x1 >= x2, 0 otherwise.

<= (x1 x2 -- n1 ) Places 1 on stack if x1 >= x2, 0 otherwise.

stringEquals (addr1 addr2 -- n1 ) Places 1on stack if the string at addr1 = string at addr2, 0 otherwise.

+ ( x1 x2 -- x1+x2 )

- ( x1 x2 -- x1-x2 )

* ( x1 x2 -- x1*x2 )

KnownBug: TinyXML also barfs when you give it a '/', so you must now use 'divide' instead of '/' for division:

/ ( x1 x2 -- x1/x2 )

divide ( x1 x2 -- x1/x2 )

dup ( x1 -- x1 x1 ) Duplicates top number on the stack.

fetch ( addr -- x1 ) Pops an address off the stack, pushes the value of the data cell at that address onto it.

store ( x1 addr -- ) Stores x1 at the address pointed to by addr.

arrayAdd ( addr1 addr2 N addr3 -- ) Does elment-by-element addition of the arrays stored at addr1 and addr2 and stores the result in the array starting at addr3.

arraySubtract ( addr1 addr2 N addr3 -- ) Does elment-by-element subtraction of the arrays stored at addr1 and addr2 and stores the result in the array starting at addr3.

arrayMultiply ( addr1 addr2 N addr3 -- ) Does elment-by-element multiplication of the arrays stored at addr1 and addr2 and stores the result in the array starting at addr3.

arrayDivide ( addr1 addr2 N addr3 -- ) Does elment-by-element division of the arrays stored at addr1 and addr2 and stores the result in the array starting at addr3.

vectorStore ( x1 x2 x3 addr -- ) Stores the three numbers at the address pointed to by addr.

vectorCopy ( addr1 addr2 -- ) Copies a 3-element vector from addr1 to addr2.

vectorAdd ( addr1 addr2 addr3 -- ) Adds 3-element vectors at addr1 and addr2, stores the result at addr3.

vectorSubtract ( addr1 addr2 addr3 -- ) Subtracts 3-element vector at addr2 from that at addr1, stores the result at addr3.

vectorScale ( x addr1 addr2 -- ) Scalar-vector multiplication: multiplies vector at addr1 by x, stores the result at addr2.

vectorMagnitude ( addr1 -- x ) Pushes the magnitude of the vector at addr1 onto the stack.

vectorNormalize ( addr1 addr2 -- ) Normalizes the vector at addr1 and stores the resulting vector at addr2.

vectorTransform ( addr1 addr2 addr3 -- ) Matrix-vector multiplication: multiplies vector at addr2 by matrix at addr1, stores the result at addr3.

matrixStore ( x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12 x13 x14 x15 x16 addr -- ) Pops 16 numbers and an address off the stack, stores the numbers in the data cell pointed to by addr. Note that matrix indices go down the columns first.

matrixStoreTranspose ( x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12 x13 x14 x15 x16 addr -- ) Pops 16 numbers and an address off the stack, stores the numbers in the data cell pointed to by addr after transposing the resulting matrix. This allows you to enter a 4x4 matrix in a PForth program in a nice readable way, i.e. entering the matrix in 4 rows and 4 columns in a text editor and storing it with matrixStoreTranspose will give you the matrix as it looked in the editor.

matrixCopy ( addr1 addr2 -- ) Copies matrix at addr1 to addr2.

inverseMatrix ( addr1 addr2 -- ) Computes inverse of matrix at addr1 and stores it at addr2. Erratum: The docs used to incorrectly 'matrixInverse' instead of 'inverseMatrix'; corrected 3/3/06.

matrixMultiply ( addr1 addr2 addr3 -- ) Multiplies matrix at addr1 by matrix at addr2 and stores the result at addr3.

concatMatrices ( addr1 addr2 ... addrN numInputMatrices addrOut -- ) Multiplies several matrices together from left to right, i.e. mOut = m1*m2*...*mN, and stores the result at addrOut.

translationMatrix ( x y z addr -- ) Constructs translation matrix for offsets x, y, and z, and stores at addr.

translationMatrixV ( addr1 addr2 -- ) Generates a translation matrix for the vector at addr1 and stores it at addr2.

rotationMatrix ( angle axis addr -- ) Constructs rotation matrix for rotation by angle degrees about axis (0(x)-2(z), use constants below) and stores it at addr.

xaxis , yaxis , zaxis ( -- n1 ) Constants for use with rotationMatrix.

rotationMatrixV ( x addr1 addr2 -- ) Generates a rotation matrix for a rotation through angle x (degrees) about the vector at addr1 and stores it at addr2.

rotationMatrixVectorToVector ( addr1 addr2 addr3 -- ) Generates a rotation matrix to rotate the vector at addr1 to the vector at addr2 and stores it at addr3.

extractTranslation ( addr1 addr2 -- ) Extracts the translation vector from the matrix at addr1 and stores it at addr2.

extractTranslationMatrix ( addr1 addr2 -- ) Extracts translational component (matrix) of matrix at addr1 and stores it at addr2.

extractRotationMatrix ( addr1 addr2 -- ) Extracts rotational component (matrix) of matrix at addr1 and stores it at addr2.

stack ( -- ) Prints the contents of the stack to the standard error.

clearStack ( whatever -- ) Empties the stack.

dataspace ( -- ) Prints contents of dataspace.

printString ( addr -- ) Prints string at addr.

printVector ( addr -- ) Prints vector at addr.

printMatrix ( addr -- ) Prints matrix at addr.

printArray ( addr N -- ) Prints N-element array starting at addr.

Event-filtering Vocabulary

These are words for processing arInputEvents. They are meant to be used with the arPForthFilter to modify an input-event stream. PForth event-filtering code is generally embedded in an input-device record in a Syzygy parameter file (see  Syzygy Configuration.

Input events can be filtered based on their type and index. There are three types of input events containing different types of values:

  1. Button events contain an integer that is 0 or 1.
  2. Axis events contain a floating-point number that usually (but not always) represents the state of a joystick.
  3. Matrix events contain a 4x4 matrix floats representing position and orientation of a tracking sensor.

     

    The event index is used to distinguish events from different sources, e.g. different tracking sensors get mapped to matrix events with different indices.

     

    See Syzygy Input Framework: Overview for more information.

     

    You define an event filter by writing a PForth program that defines one or more words whose names match certain patterns. These words are then called by the filter when the event type and index match the pattern of a word you have defined. To wit:

     

  4. The word filter_all_events will be called for every input event.

     

  5. The words filter_all_buttonsfilter_all_axes, and filter_all_matrices will be called for each incoming event of the appropriate type, regardless of its index.

     

  6. Words matching the pattern filter_<event_type>_<event_index> will be called when an event of the appropriate type and index comes in. For example, to apply a filter only to the head matrix (matrix event #0), define the word filter_matrix_0Gotcha: The entire sequence of words to call for a given event is computed before any of them are called. This means that if you change the index of an event in one of the filter_all_... words, the word filter_<event_type>_<new_event_index> won't be called, instead filter_<event_type>_<original_event_index> will be called.

     

    If a given event matches the pattern for more than one word (e.g. a matrix event #0 comes in and you've defined the words filter_all_matrices and filter_matrix_0), then both will be called, with the more general one (filter_all_matrices) coming first.

     

    In all these cases, the word can access the current event, and the most recent state of all other events, via these words:

Current-Event Words

getCurrentEventIndex ( -- n1 ) Places the index of the current event on the stack.

getCurrentEventButton ( -- n1 ) If the current event is a button event, places the button value on the stack. If it's not a button event, it throws an exception (which aborts the current PForth program ) and prints an error message.

getCurrentEventAxis ( -- x1 ) If the current event is an axis event, places the axis value on the stack. If it's not an axis event, it throws an exception and prints an error message.

getCurrentEventMatrix ( addr -- ) If the current event is a matrix event, it pops an address off the stack and attempts to copy the matrix to that location in the dataspace. If it's not a matrix event, it throws an exception and prints an error message.

setCurrentEventIndex ( n1 -- ) Sets the index of the current address to n1 .

setCurrentEventButton ( n1 -- ) Sets the current event's value to n1 if it's a button event, otherwise throws an exception and prints an error message.

setCurrentEventAxis ( x1 -- ) Sets the current event's value to x1 if it's an axis event, otherwise throws an exception and prints an error message.

setCurrentEventMatrix ( addr -- ) Sets the current event's value to the matrix at location addr in the dataspace if it's a matrix event, otherwise throws an exception and prints an error message.

deleteCurrentEvent ( -- ) Flags the current event for deletion by the filter.

Event-State Words

getButton ( n1 -- n2 ) Gets the value of button event # n1 and pushes it on the stack. Returns 0 if that button event doesn't exist.

getOnButton ( n1 -- n2 ) Returns (pushes on the stack) 1 if button event # n1 has just transitioned from 0 to 1 (i.e. the button has just been pressed) and 0 otherwise. Returns 0 if that button event doesn't exist.

getOffButton ( n1 -- n2 ) Returns (pushes on the stack) 1 if button event # n1 has just transitioned from 1 to 0 (i.e. the button has just been released) and 0 otherwise. Returns 0 if that button event doesn't exist.

getAxis ( n1 -- x1 ) Gets the value of axis event #n1 and pushes it onto the stack. Returns 0.0 if it doesn't exist.

getMatrix ( addr n1 -- ) Stores matrix event n1 in the data cells pointed to by addr.&nbsp; Returns identity matrix if event doesn't exist.

Event-Creation Words

insertButtonEvent ( n1 n2 -- ) Creates a new button event with value n1 and index n2 and inserts it into the event stream. Erratum: Arguments were listed in the wrong order. Fixed 3/3/06.

insertAxisEvent ( x1 n1 -- ) Creates a new axis event with value x1 and index n1 and inserts it into the event stream. Erratum: Arguments were listed in the wrong order. Fixed 3/3/06.

insertMatrixEvent ( addr n1 -- ) Creates a new matrix event with value taken from the dataspace at addr and index n1 and inserts it into the event stream. Erratum: Arguments were listed in the wrong order. Fixed 3/3/06.

The Syzygy Database Vocabulary

These words access the Syzygy database either explicitly or implicitly:

getStringParameter ( addr1 addr2 addr3 -- ) Uses the string at addr1 as the group name and the string at addr2 as the parameter name, and stores the returned value at add3. For example, if addr1 pointed to "SZG_HEAD" and addr2 pointed to "fixed_head_mode", then after execution addr3 would point to either "true" or "false" (assuming it was set).

getFloatParameters ( addr1 addr2 n1 addr3 -- ) Uses the string at addr1 as the group name, the string at addr2 as the parameter name, and n1 as the number of values to expect. addr3 should point to an array of the correct size to hold the parameters For example, if addr1 pointed to "SZG_HEAD" and addr2 pointed to "eye_direction", then after executing "addr1 addr2 3 addr3 getFloatParameters", the array at addr3 would contain the 3 elements of the eye direction vector.

Special-purpose Words

These words can replace the two C++ filters in arTrackCalFilter.cpp:

initTrackerCalibration ( -- ) Loads our position-correction lookup table for the MotionStar tracker. It reads the same database parameters as the C++ version (see szg/src/drivers/arTrackCalFilter.cpp): It looks for a file with the name specified by SZG_MOTIONSTAR/calib_file (on the path specified by SZG_DATA/path). The file format is the same also. After reading the lookup table, it adds this word to the dictionary:

doTrackerCalibration ( addr1 addr2 -- ) Reads an input matrix from addr1 and uses the lookup table to correct the positional components, then stores the result at addr2.

initIIRFilter ( -- ) Initializes a positional IIR filter. It reads the same database parameters as the C++ version (see szg/src/drivers/arTrackCalFilter.cpp): It reads 3 floats specified by SZG_MOTIONSTAR/IIR_filter_weights (filter weights for x, y, and z), then adds this word to the dictionary:

doIIRFilter ( addr1 addr2 -- ) Reads an input matrix from addr1 and applies an IIR filter the positional components, then stores the result at addr2. The filter is output[i] = (1-w)*input[i] + w*output[i-1], where output[i-1] denotes the output from the previous event.

Using the arPForth Object

This section is about embedding a PForth filter in a C++ program. If you're just planning on using PForth in conjunction with an input device driver to be loaded by DeviceServer, you don't need to know this.

If you don't need to install new C++ actions, there are only a few arPForth methods that you need to know:

bool arPForth::operator!() (as in !pforth) returns false if initialization of the arPForth object was successful.

bool arPForth::compileProgram( const string sourceCode ) compiles a program and stores it internally.

arPForthProgram* arPForth::getProgram() returns a pointer to the current program so that you can re-run it later without recompiling. Note that from this point on you own the pointer and are responsible for deleting it. The program is cleared from internal memory.

bool arPForth::runProgram() runs the internally-stored program.

bool arPForth::runProgram( arPForthProgram* program ) runs the compiled program pointed to by 'program'.

vector<string> arPForth::getVocabulary() returns the entire vocabulary. This is used in the arPForthFilter. The idea is, you define filter words whose names match a particular pattern. The filter extracts them from the vocabulary and creates compiled programs for each of them, so that it can run the appropriate program whenever it comes across a particular input event type.

Tracker Coordinate Conversion

Swapping Axes

Unless you're more mathematically sophisticated than we are, this is the tricky bit. You've got your tracking device talking with a Syzygy device driver, but its native coordinate axes point in different directions from the Syzygy coordinate system (which, like OpenGL coordinates, is +X=right, +Y=up, +Z=back when you are facing in your VR apparatus' "forwards" direction. For example, in the Beckman Institute Cube the axes are +X=East, +Y=up, +Z=south). How to get the two sets of axes aligned?

Mapping the Tracker Axes

First, if you don't already know it you need to determine what directions the tracker axes are pointed. This is easiest to do with the DeviceClient utility in 'position' mode. For example, if you're running your tracker as input service  SZG_INPUT0 in the context of a virtual computer named 'vc', then you would start DeviceClient using e.g.:

  DeviceClient 0 -position 0 -szg virtual=vc

...and it would print out a stream of positions computed from the incoming values of matrix event #0, which would correspond to the head tracking sensor (the first '0' above is the input service number, while the second is the matrix event index). Now you can move the sensor around and observe how the reported position values change from place to place.

The Axis-Transformation Equation

You perform the axis transformation by bracketing each incoming matrix event with a pair of matrices, one of which is the inverse of the other:

  M' = C * M * C-1

The C matrices are constructed such that when multiplied by M they swap two of its rows or columns (depending on whether it's left- or right-multiplication) along with an optional change of sign (multiplication by -1). Each row and column of each of these matrices has exactly one non-zero element, which is equal to 1 or -1, and the lower-right element is 1.

Determining the Axis-Transformation Matrices: An Example

Take the Ascension Flock of Birds™ tracker. It uses a right-handed coordinate system with the X-axis pointing away from the transmitter power cable and the Z-axis pointing down (i.e. towards the side containing the hole for the mounting screw). Let's say that we've got it mounted so that the power cable points forwards (away from the user), such that +X=back, +Y=left, +Z=down, and we've confirmed with DeviceClient that these are the tracker coordinate axes. We need to map these to the Syzygy coordinates +X=right, +Y=up, +Z=back. In other words we want:

  Tracker X => Syzygy +Z
  Tracker Y => Syzygy -X
  Tracker Z => Syzygy -Y

We construct the transformation matrix using the table below. The tracker axes go along the top and the desired Syzygy axes along the side, with the sign of the non-zero element corresponding to the sign of the mapping:

Tracker XTracker YTracker Z
Syzygy X0-100
Syzygy Y00-10
Syzygy Z1000
 0001

i.e.

and because these are orthonormal matrices, the inverse is the same as the transpose, i.e.:

Implementing the Axis Transformation in a PForth Filter

The best way to implement these transformations is in a PForth filter inside your global input device definition. Here's an example using the above transformations:

<param>
  <name>
    fob_tracker
  </name>
  <value>
    <szg_device>
      <input_sources> arBirdWinDriver </input_sources>
      <input_sinks></input_sinks>
      <input_filters></input_filters>
      <pforth>
        /* Declare matrix variables (each 'matrix' call allocates
           16 cells in the dataspace and defines a new word, e.g.
           'Xin', that pushes the address of the first cell onto
           the stack. */
        matrix Xin
        matrix Xout
        matrix C
        matrix Cinv

        /* Store transformation matrices. */
        0 -1  0  0
        0  0  -1 0
        1  0  0  0
        0  0  0  1
        C matrixStoreTranspose

        0 -1  0  0
        0  0  -1 0
        1  0  0  0
        0  0  0  1
        Cinv matrixStore

        /* Define 'filter' word to be called when any matrix event
           passes through the filter. */
        define filter_all_matrices
          Xin getCurrentEventMatrix
          /* Multiply C * Xin * Cinv, store result in Xout */
          C Xin Cinv 3 Xout concatMatrices
          Xout setCurrentEventMatrix
        enddef
      </pforth>
    </szg_device>
  </value>
</param>

 

This device definition loads one device driver plugin (shared library): arBirdWinDriver, the Syzygy Flock-of-Birds™ driver that is based on the Bird.dll supplied by Ascension (Windows only). It also defines a PForth filter to be applied to the output of this device.

PForth (or PseudoForth) is a FORTH-like language. This is, it's stack-based and uses RPN notation like an HP calculator. For example, to add two numbers together you would type 3 2 + ("Place the numbers 3 and 2 on the stack, then call the '+' word, which takes the top two numbers off the stack and pushes their sum onto the stack"). PForth is compiled; when the PForth filter is loaded, the source code gets converted into an STL vector<> of pointers to objects, one for each PForth word. Running a filter word consists in iterating through its vector<> of pointers and calling i->action() for each one, so it's quite fast.

The filter defines four matrix variables, XinXoutC, and CinvXin and  Xout are just temporary storage for the initial and final values of the tracker matrices. C and Cinv are the two axis-transformation matrices we constructed above. It may be a bit confusing that we use matrixStoreTranspose to store the values in C and matrixStore for  Cinv. In Syzygy, as in OpenGL, matrix values are internally stored in a one-dimensional array with the row subscript varying faster, i.e. going down the first column, then the second, and so on. The  matrixStore word reads in the numbers and stores them in the same order: In other words, the numbers in the top row of the matrix text above end up being stored internally as the first column of the matrix, so you end up with the transpose of the matrix as it appears in the PForth source code. The matrixStoreTranspose word allows you to enter matrices in human-readable form.

Finally, the code defines a filter wordfilter_all_matrices, which will be applied to any outgoing matrix event. The matrix value is stored in Xin in the first line, the transformation equation is applied in the second (concatMatrices multiplies together a variable number of matrices, specified in this case by the '3'), and the result is stuffed back into the matrix event in the third.

Correcting Residual Coordinate-Axis Direction Errors

Still to do...

Specifying the Origin Offset

Suppose the aforementioned tracker transmitter is mounted two feet off the ground, so we need to add 2 to the Y position coordinate of each matrix event from the tracker. Modify the PForth program as follows:

        /* Declare matrix variables (each 'matrix' call allocates
           16 cells in the dataspace and defines a new word, e.g.
           'Xin', that pushes the address of the first cell onto
           the stack. */
        matrix Xin
        matrix Xout
        matrix C
        matrix Cinv
        matrix originOffset

        /* Store transformation matrices. */
        0 -1  0  0
        0  0  -1 0
        1  0  0  0
        0  0  0  1
        C matrixStoreTranspose

        0 -1  0  0
        0  0  -1 0
        1  0  0  0
        0  0  0  1
        Cinv matrixStore

        /* create a matrix that translates by (x,y,z)=(0,2,0) */
        0 2 0 originOffset translationMatrix

        /* Define 'filter' word to be called when any matrix event
           passes through the filter. */
        define filter_all_matrices
          Xin getCurrentEventMatrix
          /* Multiply originOffset * C * Xin * Cinv, store result in Xout */
          originOffset C Xin Cinv 4 Xout concatMatrices
          Xout setCurrentEventMatrix
        enddef

The tracker origin offset transformation comes before the rest.

Correcting Sensor Orientations

Now we have the problem of attaching tracking sensors to things that we want to track. The natural orientation for the Flock-of-Birds™ sensors is base-down with the cord pointing forwards. That will most likely not be a convenient way to mount them. Let's say that we have two of them and we want to mount one on the left side of a pair of glasses with the base facing right and the cord facing the back; this sensor will provide matrix event #0. The other one will be mounted on the bottom of a gamepad with the base facing up and cord facing back; this will provide matrix event #1.

The first mounting transformation can be composed of a 180-degree rotation around the Y axis (to get the cord pointing backwards) followed by a -90-degree rotation around the Z-axis (remember, after the first rotation the positive Z axis points forwards).

The second one is a 180-degree rotation around Y followed by another 180-degree rotation around Z.

We extend the PForth filter as follows:

        /* Declare matrix variables (each 'matrix' call allocates
           16 cells in the dataspace and defines a new word, e.g.
           'Xin', that pushes the address of the first cell onto
           the stack. */
        matrix Xin
        matrix Xout
        matrix C
        matrix Cinv
        matrix originOffset

        /* Store transformation matrices. */
        0 -1  0  0
        0  0  -1 0
        1  0  0  0
        0  0  0  1
        C matrixStoreTranspose

        0 -1  0  0
        0  0  -1 0
        1  0  0  0
        0  0  0  1
        Cinv matrixStore

        /* create a matrix that translates by (x,y,z)=(0,2,0) */
        0 2 0 originOffset translationMatrix

        /* Define 'filter' word to be called when any matrix event
           passes through the filter.  Applies transmitter coordinate
           transformations (common to all sensors) */
        define filter_all_matrices
          Xin getCurrentEventMatrix
          /* Multiply originOffset * C * Xin * Cinv, store result in Xout */
          originOffset C Xin Cinv 4 Xout concatMatrices
          Xout setCurrentEventMatrix
        enddef

        matrix ySensorRot
        matrix zHeadRot
        matrix zGPadRot
        matrix headRot
        matrix GPadRot

        /* Construct 3 rotation matrices, 2 containing separate
           Z-rotations for head and gamepad and a common Y-rotation */
        180 yaxis ySensorRot rotationMatrix
        -90 zaxis zHeadRot rotationMatrix
        180 zaxis zGPadRot rotationMatrix

        /* Pre-construct the head and gamepad transformations */
        ySensorRot zHeadRot headRot matrixMultiply
        ySensorRot zGPadRot GPadRot matrixMultiply

        /* Define filter words to be called when matrix events with
           particular indices (i.e. that originate from particular
           sensors) pass through. They apply mounting transformations
           for individual sensors */
        define filter_matrix_0
          Xin getCurrentEventMatrix
          Xin headRot Xout matrixMultiply
          Xout setCurrentEventMatrix
        enddef
        define filter_matrix_1
          Xin getCurrentEventMatrix
          Xin GPadRot Xout matrixMultiply
          Xout setCurrentEventMatrix
        enddef

 

All of the new code is at the bottom. We need five additional matrices, one for each of the three rotation components discussed above and two more to hold the concatenation of the two components for the head and for the gamepad. We could have left the components separate and multiplied them inside the define/enddef blocks using  concatMatrices, but that would have been less efficient; better to perform the multiplication once at compile time when the filter is initialized.

The filter_matrix_# words are called for matrix events with the appropriate index # after filter_all_matrices is called.

vrtest, a "sanity check" application

The utility ``vrtest'' helps you verify that all the parts of a Syzygy installation are operating correctly -- an end-to-end test, from the user's actions all the way through to what they hear and see. When diagnosing misbehavior in a Syzygy application, a few seconds running vrtest will greatly narrow down the fault.

History

This application was called ``cubevars'' for several years. That name alluded to its inspiration, the early 1990's IRIX CaveLib program ``cavevars.'' Cubevars departed from cavevars in abandoning numerical displays for purely graphical ones.

 

Some of the graphics in vrtest deliberately hearken to those of the Input Simulator.

Fixed Graphics

The large white wireframe cube indicates a cave-sized space, ten feet on a side. The origin is in the middle of the floor, in the usual cave coordinate frame.

On each wall of this cube, you'll see some text and a stylized pair of eyeglasses. The text label indicates which wall it is (front, right, ceiling, etc.). The eyeglasses indicate monoscopic or stereoscopic vision. If both lenses are visible, the display is monoscopic. If the display is stereo, when you close one of your eyes, the corresponding lens of the eyeglasses should vanish. (If it doesn't, that indicates a problem with your LCD shutter glasses, the shutter-glasses sync pulse, or (in a passive-stereo display) the polarizers.

Head

The user's head appears as a yellow ball with cyan eyes and red pupils.

An indigo wireframe box is attached to the user's gaze vector (stuck in front of your nose). If you aren't always looking directly into the end of the box, head tracking is malfunctioning.

Wand

The wand is a long wireframe cone, magenta in color. The cone's point indicates the wand's ``forward'' direction. The wand's position is where the cone meets a cyan crosshairs. One of the crosshairs is longer and ends in a white wireframe ball; this indicates the wand's ``up'' vector.

Multiple wands will be drawn, if the virtual computer's definition includes them. Any 6DOF-tracked item (``matrix'') other than the first, which is the head, is considered to be a wand.

Buttons and Joysticks

Near the wand, all of its buttons and ``axes'' (joysticks and sliders) are drawn.

The buttons are dark red cubes. When you push a button, its corresponding cube becomes larger and turns pale green.

The axes are green cubes on brown vertical sliders. The first two axes are redundantly drawn as a single cube in a square, since these axes usually correspond to a single x-y joystick on the wand.

 

Wall Displays

The utility ``vrtest'' helps you verify that all the parts of a Syzygy installation are operating correctly -- an end-to-end test, from the user's actions all the way through to what they hear and see. When diagnosing misbehavior in a Syzygy application, a few seconds running vrtest will greatly narrow down the fault. History This application was called ``cubevars'' for several years. That name alluded to its inspiration, the early 1990's IRIX CaveLib program ``cavevars.'' Cubevars departed from cavevars in abandoning numerical displays for purely graphical ones. Some of the graphics in vrtest deliberately hearken to those of the Input Simulator. Fixed Graphics The large white wireframe cube indicates a cave-sized space, ten feet on a side. The origin is in the middle of the floor, in the usual cave coordinate frame. On each wall of this cube, you'll see some text and a stylized pair of eyeglasses. The text label indicates which wall it is (front, right, ceiling, etc.). The eyeglasses indicate monoscopic or stereoscopic vision. If both lenses are visible, the display is monoscopic. If the display is stereo, when you close one of your eyes, the corresponding lens of the eyeglasses should vanish. (If it doesn't, that indicates a problem with your LCD shutter glasses, the shutter-glasses sync pulse, or (in a passive-stereo display) the polarizers. Head The user's head appears as a yellow ball with cyan eyes and red pupils. An indigo wireframe box is attached to the user's gaze vector (stuck in front of your nose). If you aren't always looking directly into the end of the box, head tracking is malfunctioning. Wand The wand is a long wireframe cone, magenta in color. The cone's point indicates the wand's ``forward'' direction. The wand's position is where the cone meets a cyan crosshairs. One of the crosshairs is longer and ends in a white wireframe ball; this indicates the wand's ``up'' vector. Multiple wands will be drawn, if the virtual computer's definition includes them. Any 6DOF-tracked item (``matrix'') other than the first, which is the head, is considered to be a wand. Buttons and Joysticks Near the wand, all of its buttons and ``axes'' (joysticks and sliders) are drawn. The buttons are dark red cubes. When you push a button, its corresponding cube becomes larger and turns pale green. The axes are green cubes on brown vertical sliders. The first two axes are redundantly drawn as a single cube in a square, since these axes usually correspond to a single x-y joystick on the wand. Wall Displays On the front wall, three dark blue squares show views of the white wireframe cube from above, from the left, and from behind. This is as if you tilted your head down to look at the floor, or turned left to face the left wall, or simply stayed put and looked at the front wall. These projected views help you visualize where the head and wand really are in the cube, particularly when tracking is malfunctioning and, e.g., the head is near the floor as shown here, or even several feet underground. Sounds If the virtual computer's SoundRender program is running and the audio hardware is working, you hear a short sound whenever you depress or release a button. Also, as you wave the wand around, you hear a quiet rumbling whose loudness varies with the speed of the tip of the purple cone. 

On the front wall, three dark blue squares show views of the white wireframe cube from above, from the left, and from behind. This is as if you tilted your head down to look at the floor, or turned left to face the left wall, or simply stayed put and looked at the front wall.

These projected views help you visualize where the head and wand really are in the cube, particularly when tracking is malfunctioning and, e.g., the head is near the floor as shown here, or even several feet underground.

Sounds

If the virtual computer's SoundRender program is running and the audio hardware is working, you hear a short sound whenever you depress or release a button. Also, as you wave the wand around, you hear a quiet rumbling whose loudness varies with the speed of the tip of the purple cone.

Miscellaneous Application Features

To pause a distributed scene graph application:

  dmsg X pause on

If the application is distributed scene graph, X is the ID of an szgrender. If the application is master/slave, X is the component ID of one of the application instances.

To resume running:

  dmsg X pause off

 

To throttle/unthrottle the application's framerate to 5 fps:

  dmsg X delay on
  dmsg X delay off

 

To change the view mode:

  dmsg X viewmode anaglyph

For viewmode's values, see Viewport Lists.

To take a screenshot from raster position (A,B), with width C and height D:

  dmsg X screenshot A/B/C/D

The screenshot is saved in SZG_DATA/path with filename screenshot.Y.ppm, where integer Y is how many screenshots have been taken so far by X. (So avoid interleaving screenshot commands from multiple components, lest the files clobber each other.) Here's an example screenshot:

To send a message to your application that you can handle in your own code:

  dmsg X user blahblahblah

Your app's onUserMessage() method (userMessageCallback, for old-style apps) will be called with "blahblahblah".

To send single keypresses to your app (if e.g. you use the onKey() callback to change your app's state in standalone mode and can't be bothered to modify it for cluster mode):

  dmsg X key whee

...will cause the app's onKey() method to be called four times, once each with 'w', 'h', 'e', 'e'.