Richard C. Waters and David B. Anderson
This document is available as "http://www.merl.com/opencom/opencom-c-api.html".
This version of the document was created on February 19, 1997.
Copyright 1997, MERL - A Mitsubishi Electric Research Lab. All rights reserved.
This is a machine generated document created from a database of information. It exists in both paper and HTML form. Other documents describe the API exported to other programming language environments.
The API also includes the following ordinary data structures:
- - spWM - A world model.- - spFn - Operation that can be mapped.- - spMask - World model view mask.- - spTransform - Position, axis, angle, scale specification.- - spVector - 3-element vector.- - spRotation - Rotation specified using axis and angle.- - spQuaternion - A quaternion.- - spMatrix - 4x4 transformation matrix.- - spFormat - Sound data format.Finally, the API includes the following shared object classes:
- - sp - Top of shared object hierarchy.- - - spRegion - Basis of location-addressable communication.- - - spLink - Link to large slowly-changing data.- - - - spVisualDefinition - Link to graphic model.- - - - spSound - Link to stored sound.- - - - spBoundary - Link to bounding box.- - - - spClass - Link to description of a shared class.- - - spThing - Thing in the virtual world.- - - - spRoot - Root of recognized whole.- - - - - spAvatar - Whole representing user or agent.- - - - spAudioSource - Source of sound data.- - - - spBeacon - Basis of content-addressable communication.- - - - - spSpeaking - Connects to user's speech.- - - - - spHearing - Connects to user's ears.- - - - - spSeeing - Connects to user's eyes.- - - - spPOV - Point of view.- - - - spAction - Program triggered by system core.- - - - - spCallback - General callback.- - - - - - spIntervalCallback - Interval callback.- - - - - - spBeaconExamine - Inspect beacons.- - - - - spRemoteAction - Remotely triggered programs.- - - - - - spOwnershipRequest - Requests getting ownership.The document is organized around the various classes enumerated above. At the top level, there is a section for each class. Within these sections, there are subsections corresponding to each instance variable and method.
There are two ways to look up instance variables and functions in this document. By using the table of contents, you can look them up by the name of the class. By using the quick reference index, you can look them up alphabetically by the name of the instance variable or method.
There are several quite different kinds of classes listed above: , passive data structures, and shared objects that are the basis of communication between processes.
The passive data structures (e.g., spTransform) are pieces of data that are stored in shared objects or used in intermediate computations. In the C interface, these items are not objects, but rather just simple vectors and structures.
The shared objects consist of the class sp and its subclasses. These classes receive specialized treatment by the system. In particular, instances of the shared classes are communicated between processes. Applications can define new shared classes using a Java interface supported by a special Java preprocessor.
Most of this document consists of descriptions of the shared classes. The first line of each section for a shared class shows the declaration used in the Java interface to define the class. This is followed by a brief discussion of the class and a listing of the externally available shared instance variables and functions in the class. Subsections of the section for a class describe each of its instance variables and functions in detail.
The first line of a subsection discussing an instance variable of a shared object shows the declaration used in the Java interface to define the variable. This line includes both the type of the variable in Java (in the middle) and the type in C (in a comment at the end of the line). From the perspective of this document, only the C type is relevant. The variable declaration line is followed by a general discussion of the instance variable and the various access functions available for the variable.
The first line of a subsection discussing a function is the ANSI C signature of the function. This is followed by a summary of what the function does.
The names of C functions in the API are derived using the following naming scheme. If a class spK contains a method M, then the corresponding C function has the name spKM. Since every shared class name begins with the letters ``sp'', every C function in the API begins with the letters ``sp''.
The sections on non-shared classes have much the same format as the sections on shared classes.
At various places in this document, topics of interest are discussed at length in subsections. One reason for gathering information into a separate section is so that it can be referred to from many places in the document.
Applications do not communicate directly with each other, but rather
only with the world model. This allows applications to be written
without thinking about how communication is achieved. An application
does exactly the same things when it is interacting with an
application running in shared memory on the same machine as it does
when interacting with an application connected via the Internet.
Figure 1: The programming model.
The world model is not a scene graph, but rather an object-oriented
database that does not consider one kind of content to be any more
important than another. In particular, we believe that audio
information and autonomous behavior are at least as important as
visual information and should not be limited by constraints inherited
from visual rendering.
The world model specifies what objects exist in the virtual world,
where they are, what they look like, and what sounds they are making.
The world model does not contain historical information, but rather
just a snapshot of what the virtual world is like at the current
moment. As the virtual world changes second by second, the world
model changes.
The emphasis in the design of the world model is on the term
database, not object oriented. The objects have methods
associated with them, but by far the dominant operations consist of
reading and writing data stored in the instance variables of world
model objects.
Applications observe the virtual world by retrieving
data from the world model. Applications affect the virtual world by adding, removing, and
modifying objects in the world model. To avoid readers/writers
conflicts, each object in the world model has one process as its owner
and only the owning process can modify it. However, the ownership of
an object can be transferred from one process to another.
By itself, the system does not cause objects to persist over time. An
object exists only so long as the process that owns it runs. To have
persistent objects, an application must provide persistent processes
that accept ownership of these objects. These processes could make
use of a persistent file format for objects to provide efficient long
term support for infrequently visited parts of a virtual world.
Figure 2: The communication model.
A central feature of the system is that it is designed to be scalable
to a large number of users (e.g., thousands) interacting in real time.
Two key features support this: providing only approximate equality of
local world model copies and dividing the world model into chunks each
of which is communicated only to the small group of users that are
actually interested in it, rather than to all the users of the world
model.
Distributed databases typically require that all local copies of the
database must agree exactly on the information in the database.
However, this requires object locking and handshaking that is
incompatible with real-time interaction if there are more than a very
small number of users. In contrast, the focus here is on real-time interaction at all costs, providing only approximate equality between world model copies.
The primary way in which world model copies are only approximately
equal is that different users observe things as occurring at slightly
different times. We call this a relativity model of
communication and is actually not unlike the real world. When you
hear sounds from distant sources, you do not hear the sound that is
being made now, but rather the sound that was made seconds ago. As a
result, people in different locations do not hear the same things at
the same time. How great the differences are depends on how far apart
the sound sources and people are.
Similarly, when an application process finds out about a world model
change, it is not finding out about a change that is happening now,
but rather about one that happened some time ago. How long ago
depends on the network distance between the two processes. In
general, this distance is not more than a couple hundred milliseconds
and does not lead to world model differences that are unduly large.
Having only approximate equality of world model copies allows
real-time interaction, but does not of itself prevent the computation
required to maintain each local world model copy from growing in
proportion to the total number of simultaneous users of a virtual
world. To prevent this, the world model is broken up into many small
chunks called regions and information about a given region is
communicated only to the small number of users that are near enough to
that region to be interested in it. Each region is associated with a
separate communication channel so that processes that are
not interested in a region do not have to expend any processing
ignoring it. This allows the system to scale based solely on the
maximum number of users that are gathered in any one region, rather
than on the total number of users in the virtual world.
Figure 3: Servers.
In the lower right of the figure is the session manager. It
handles the connection of new users to an on-going session and their
eventual disconnection. Its workload is proportional only to the
number of users that enter or leave the session in a given minute, not
the total number of connected users.
One or more servers are included in a Spline session to act as:
user servers, region servers, beacon servers, and contact points. For flexibility, there is only one kind of server process, which can act in any of the above roles, or several at once.
The purpose of a user server is to support users with
slow network connections (e.g., modems) that do not allow them to
operate as first class peers. When acting in this role, a server intercepts all
communication to and from the user. The message traffic to the user
is compressed to take maximum advantage of the bandwidth available.
As part of this, audio streams are combined and localized before
sending them to the user. Servers are replicated as needed so that no
one server has to support more users than it can handle. Two servers acting in this capacity are
shown in Figure 3.
A region server maintains a record of everything in a given region.
When the locus of attention of a user process enters a new
region, the appropriate region server is queried in order to obtain initial information about the state of objects in the region. After this initial download, the user process obtains further incremental information by peer-to-peer communication. Responsibility for regions is parceled out among a set of servers,
which is made large enough that no one server is responsible for a
larger piece of the virtual world than it can easily handle.
Because the world model copy in an application process only contains
information about the objects in the regions the process is attending
to, there has to be an explicit mechanism for locating far away
objects in the virtual world. This is done by having the servers provide a name service that makes it possible to locate specialized objects called
beacons by name no matter where they are in the virtual world. When a user process wants to locate a particular beacon object, it consults the appropriate beacon server in order to find the beacon. As with regions, responsibility for the beacon name space is parceled out among a set of servers, which is made large enough that no one server is responsible for more beacons than it can easily
handle.
To simplify the communication between user processes and the various servers it has to interact with, each user process has a server assigned to it that acts as a sole contact point for the process. Every message from a user process (or user server acting on its behalf) that requests a service, is sent to the contact point. This allows the user process to always operate as if there was only one server. The various complexities that arise when servers are replicated are handled by the contact point, which decides where to route the messages it receives from user processes.
The session manager monitors the servers in a session and if it detects that one has crashed, restarts it. This is facilitate by the fact that the user processes and the session manager are the only sources of information. The various servers merely act as repositories of information obtained from elsewhere and can straightforwardly be reinitialized if they need to be restarted.
As shown in Figure 3, the system utilizes a hybrid communication model
in which client/server communication is primarily point-to-point but
server/server communication is peer-to-peer. In addition, users with
sufficiently fast network connections (e.g., users on corporate
intranets) can interact directly with each other and the servers using
peer-to-peer communication.
Figure 4: An application process.
The foundation of the system is the inter-process communication
module shown at the bottom of Figure 4. It provides all the processing necessary to maintain
approximate consistency between the world model copies associated with
a group of communicating processes, sending messages describing
changes in the world model caused by the local application and
receiving messages from other processes about changes made remotely.
The network interface specifies the format of these messages. Any
process that obeys this interface can interoperate.
The messages sent are of three kinds, corresponding to three kinds of
data in the world model: small rapidly changing objects, large slowly
changing objects, and continuous streams of data. An important
feature of the system is that it includes an efficient scheme for
synchronizing these different kinds of data.
The most prevalent kind of object in the world model is small things
that can change rapidly. For example, an object representing
something in the virtual world (e.g., a chair) requires only a small
description---i.e., to specify its position and orientation, whether
it is contained in some other object, and which appearance should be
used when displaying it. The features of small objects can be changed
very rapidly.
Messages describing changes in small objects are sent using
User Datagram Protocol (UDP) messages. This allows them to be
communicated very rapidly. The objects must be small enough so that
a message describing one will fit in one UDP message.
Graphic models, recorded sounds, and behaviors are represented using
large objects. These objects are identified by Universal Resource
Locators (URLs) and communicated using standard World Wide Web
protocols. Standard formats are used (e.g., VRML for graphic models, WAVE for sounds, and Java for behaviors)
so that standard tools can be
used. There is no limitation on the size of the
large objects, but several seconds can be
required to communicate one. Fortunately, since these objects change
infrequently, this latency can generally be masked by preloading the
objects before they need to be used.
The final kind of object in the world model corresponds to continuous
streams of data such as sound captured by a microphone. These streams
are communicated in small chunks using UDP messages. At the moment,
video streams are not supported. When they are, they will be
communicated in a similar fashion, but of necessity using larger
messages.
A central feature of the system is that using the various messaging
approaches above, every kind of data in the world model can be
communicated between processes. Therefore, applications can modify
and extend every aspect of a virtual world. Furthermore, while it is
often advantageous to prestore data for an application to use (i.e.,
by delivering it on a CD-ROM) this is not necessary. Once started, an
application will fetch everything it needs that has not been
prestored.
Figure 5: Typical configuration supporting a user.
Visual and audio rendering modules are provided; however rather than
being tightly integrated with the system, they are separate
applications interfaced to the system. They use the same API as other
applications and do not have to be tightly coupled with the main
application.
One advantage of the loose coupling of renderers is that the renderers
interacting with a person can run in separate processes from the main
application. This allows them to operate on separate machines in
situations where maximum performance is required. However, the
primary mode of operation is for them to run in the same process with
the main application, sharing a single copy of the world model as
shown in Figure 5.
The greatest advantage of the loose coupling between rendering and the
system core is that the system is not tied to any one renderer.
Rather, the API is designed to be easily interfaced to almost any
renderer. Default renderers are supplied with the system, but it is
expected that demanding applications will switch to renderers that are
tuned to the task at hand.
Consider visual rendering as an example. In the world model,
objects can have positions, orientations, and appearances. The visual
renderer creates a scene graph by combining the appearances associated
with the objects that are near enough to be seen and renders the scene
graph from the vantage point specified by the application. The system
itself does nothing with the graphic models that describe the
appearances of objects. The only thing that matters is that the
visual renderer being used can load them.
Audio rendering is supported in a similar fashion. The world
model contains objects representing sources of sounds. These objects
specify the places where the sounds are located. They can either be
point sources or diffuse ambient sources. Sound to be played through
these objects can be captured live from a microphone or prestored in
recorded sound objects. The audio renderer creates an audio
scene graph by combining the sounds associated with sound source
objects that are near enough to be heard and renderers this from the
vantage point specified by the application. The system itself does
nothing with sound encodings. The only thing that matters is that the
audio renderer being used can decode them.
Figure 6: Typical configuration
supporting a simulation.
A simulation operates just like any other application using the
same API to interact with the world model. However, no visual or
audio rendering is needed, because there is no person to see or hear
it. Complex simulations,
intelligent agents, and large persistent databases can all be directly
connected to a virtual world. Large powerful computers without support for graphics or sound
can be used to manage shared content.
It is important to note that it is a great deal easier to say that a
simulation interacts with the world model just like any other
application than it is to make it possible for a simulation to
effectively interact with the world model. The reason for this is
that applications supporting a user have a human being in the loop and
simulations do not.
For example, it is typically easy for a person to look at an
avatar and determine which way the avatar is facing, based on a
rendered image. However, it verges on impossible for a program to
tell where the face of an avatar is by looking at a list of polygons.
To deal with this kind of problem, the system has been designed to
make information such as which way an avatar is facing easily
accessible to programs. Specifically, Spline's world model contains
an object class spAvatar that is used to identify avatars as opposed
to other kinds of objects and by convention the center of the coordinate system for an object is at the center of the object, the Y axis is up, and
objects face done the negative Z axis
The following paragraphs briefly describe each kind of atomic
data used in the API. Some of these types of data are described at
greater length in other sections.
Boolean values represented using the type
spBoolean. These are used to indicate True and False
values.
16-bit integers represented using the type short.
These are used when low range integers are adequate.
32-bit integers represented using the type long.
These are used for representing most integer values.
32-bit floating point numbers represented using the type
float. In keeping with standard graphics practice, these are
used to represent most non-integer values. They have limited
precision, but are compact.
32-bit unique names represented using the type
spName. The system dynamically generates names that are
unique across all the processes participating in a session.
These names are used both to identify shared objects and to identify
the processes that own the objects.
IP addresses/port pairs represented using the type spAddress.
These are used to represent network addresses. They contain two pieces of information, a 32-bit IP host address and a 16-bit port number. In C an spAddress is a struct containing the host address, followed by the port number. followed by 16 unused bits to pad the total field out to two full words.
32-bit time durations in milliseconds represented using
the type spDuration. Whenever an API function takes a
time duration as an argument, this argument is in terms of
milliseconds. For compactness, 32-bit integers are used. This allows
a range of approximately plus or minus two weeks. (This does not impose any limitation on the lifetime of a session in the virtual world, but rather only limits the maximum difference between two times that can be represented.)
World model view masks represented using the type spMask.
These bit masks are used to control the visibility of shared objects in the world model.
Audio formats represented using the type spFormat.
These values are used to represent
audio encodings and sample rates.
A key feature of the API is that pointer data is always passed
into and returned from functions by reference rather than copying.
This promotes efficiency, but means you must pay close attention to
the following.
1- The system never alters data that is passed to it via a pointer
unless this document explicitly states otherwise.
This means you can depend on the value remaining the same
unless you change it yourself.
2- The system never retains a pointer passed to it beyond the time
the function that was given the pointer returns. (If the system
needs to keep any of the data, it copies it.)
3- When a pointer is passed to you, you must never alter the data
pointed to unless it is explicitly stated that you should, because
the system depends on the data not being altered. In particular, you must
never free something referred to by a pointer unless you yourself
allocated the storage.
4- It is allowed that you retain pointers returned by an API function, but
only until the next time spWMUpdate is called. The reason
for this is that the system assumes that during calls on
spWMUpdate, it can free any storage it has allocated. The
only exception to this is certain shared objects.
If you want to save data across a call on spWMUpdate, you
must copy it.
The following paragraphs briefly describe each kind of pointer
data used by the system. Many of these types of data are described at
greater length in other sections.
A UTF8 ASCII string represented using the type char *.
This is a null-terminated string. If the characters are all 7-bit ASCII characters, then this is an entirely ordinary string. If extended characters are used, then special escape sequences are present.
An immutable spFixedAscii string containing no more than 500 characters
represented using the type spFixedAscii. This is the same as
the above, but is limited in length so that it can fit into a single
UDP message. It is used for most shared character data in shared objects.
This data must be specified when the object is initially
created and cannot be altered afterward.
A 17-element position, orientation, and scaling vector represented
using the type spTransform. This structure is used as the primary representation of the position and orientation of objects.
A 3-element vector represented using the type spVector.
Vectors containing three values are
used for several different purposes.
A 4-element quaternion represented using the type spQuaternion.
Quaternions are one way of
representing rotations.
A 4x4 transformation matrix represented using the type spMatrix.
Following standard graphics
practice, the position, orientation, and scaling of objects can be
represented using a 4x4 matrix.
A locally defined operation represented using the class
spFn. The API makes heavy use of callback functions and the mapping
of functions over objects. For convenience, a
single type is used to represent functional arguments in all these
situations.
An interaction window represented using the type
spWindow. Exactly what type this is depends on
the execution environment.
An opaque pointer represented using the type
void *. In a number of places, the system records pointers that
are not part of the external interface and are not intended to be
manipulated by applications.
All interaction with instance variables in shared objects is via access functions, rather than via direct access to the instance variables or structure fields. In the external API, the number of access functions available depends on the visibility of the instance variable in the API. The following discusses the accessors in detail for an instance variable V in a shared class spK containing data of type T.
The following accessor obtains the value of the instance variable V for an object. It reports an error if the object from which the data is being obtained is not an instance of (a subclass of) the class spK.
The following accessor is available for setting the value of V. A set accessor is available in the external API only if the external API allows the variable to be set. The Set accessor reports an error if the object to be modified is not an instance of (a subclass of) the class spK, if the object has been removed from the world model, or if the value being stored is a shared object that has been removed. If V is an instance variable that is shared between the processes in a session, then the Set accessor reports an error if the owner of the object is not equal to the current value of spWMGetMe.
A few instance variables in shared objects are constant in nature in that they cannot be altered after an object is initially created. For example, this is true for variables whose values have the C type spFixedAscii. If a shared instant variable is not constant in nature, then the following accessor is available for obtaining the value of V prior to a change. (Specifically, the accessor obtains the value V had at the end of the last call on spWMUpdate.)
The primary intended use of these accessors is in callback tests.
The accessor reports an error if the object from which the data is being obtained is not an instance of (a subclass of) the class spK.
Obtaining an old value can take substantially more time than obtaining the corresponding current value.
In the interest of saving space in this document, the discussions of individual instance variables in shared objects merely list which accessors are available with reference to this section for greater details.
It should be noted that for a given class spK, the accessors above are available not just for the variables directly defined in the class, but also for the variables that are inherited. For example, if spK inherits a variable V from a superclass spJ and the accessor spJSetV is available, then the accessor spKSetV is also available. In the interest of brevity, these inherited accessors are not explicitly listed in this documentation.
A very important feature of the API is that the local world model copy
in a given process does not contain everything in the entire virtual
world, but rather only a subset of the objects that are of local
interest. As discussed elsewhere, the determination of subsets is
based primarily on regions.
Because only some of the objects
in the world model are in the local world model copy at a given
moment, it is entirely possible that the local world model might
contain an object A that refers to an object B (e.g.,
A's Parent might be B) and yet the local world
model might not contain the object B.
Communication is arranged so that if two objects
refer to each other, then in general they are both communicated in the
same region. As a result, the kind of inconsistency described above
seldom exists for long. However, it is very common for this kind of
inconsistency to exist briefly for a variety of reasons.
(1) When the focus of interest moves into a new region, a process
inevitably hears about some objects before others. (Because there are places where circularity of references is required, nothing can avoid the fact that this can cause temporary dangling references.)
(2) When new objects are created, a process inevitably hears about some
objects before others.
(3) When objects are removed, a process may hear that an object is
gone before hearing that other objects have stopped referring to it.
The above situations are handled by essentially putting
the burden of a sanitized version of the problem on the application.
When an instance variable contains a shared object, the access
function that obtains the value (e.g.,
spGetParent) returns the object referred to only if
the object is in the local world model copy. Otherwise it returns
Null.
Suppose again that there is an object A in the local
world model copy. If spGetParent returns
non-Null when applied to A, then the return value is
the Parent of A. However, if
spGetParent returns Null, then this could
mean either that A has no Parent or merely that the Parent of
A does not yet exist in the local world model copy. (There is
no way to tell the difference between these two things without
inspecting details of A that are below the level
of the even the internal API.)
Suppose that A does indeed have a Parent B and
suppose further that A appears in the local world model before
B. When B later appears, this registers in all respects
just the same as if the Parent of A changed from Null
to B. In particular, if there is a callback watching for such a change, it will be triggered.
The above approach works well because it does not matter
to the typical application whether the Parent of A has changed
or has merely just become known. An application has to be able to
deal with arbitrary changes made by other processes, and doing this
properly typically leads to a solution that operates properly in the
presence of evolving partial knowledge as well. However, it is important to keep clearly in mind that a
process's knowledge of the world model is always partial and
therefore, it is never justified to draw more than merely provisional
conclusions from the absence of something in the world model, since anything
can appear during a call on spWMUpdate.
A particularly good example of the problems related to asynchronous changes is what must be done to properly handle the asynchronous removal of objects. To start with, it is important to understand what happens when an object is removed.
First, the IsRemoved bit is set on in the object R being removed and all connections between R and other objects are broken. Specifically, any instance variable in any other object that refers to R is set to Null. In addition, any instance variable of R that refers to any other shared object is set to Null. A result of this is that having an object's Parent removed is treated very much the same as if the object's Parent was simply changed to Null.
After the first stage of removal above, one can still consult all the instance variables of R, with the proviso that the variables that point to other objects have all become Null. (For variables that are shared with other processes, you can consult the old values of the instance variables to find out what they used to point to.)
Second, some time later (exactly how much time later is discussed in detail below) the storage corresponding to object R is freed. After that time, one can no longer access any of the instance variables of R, and it can be disastrous to try. A key purpose of the first step of removal is to ensure that no other shared object can retain a pointer to a freed object. Applications must be sure that they don't either. There is a separation in time between the first and second stages of removal so that applications have time to notice when objects have been removed.
To deal with asynchronous object removal, an application must check on a regular basis whether any objects it is interested in have been removed and respond appropriately before the objects are freed.
The system has carefully designed rules about when objects can be removed and freed in order to reduce the number of times an application has to check for an object's continued existence.
Between calls on spWMUpdate, objects are never removed unless they are explicitly removed by the local process. The basic result of this is that it is sufficient for a process to check for the continued existence of an object it is keeping track of once each time after spWMUpdate returns. This checking is done using the accessor spGetIsRemoved. Once an object has been removed, the local process should drop all pointers to it immediately, because they will very soon be invalid. (A particularly good way to do the checking above is to use an spJustRemoved callback on the object in question.)
Objects are never freed except during calls on spWMUpdate. In addition, if an object is removed after the beginning of a call on spWMUpdate it is not freed until the very end of the next call on spWMUpdate. This means that for every object, there will be at least one period between spWMUpdate calls where the object has been removed and not yet freed. This gives the application an opportunity to detect this fact and do something about it. (It also guarantees that the object stays around long enough for any spJustRemoved callbacks to get triggered.)
As a result of the above, C applications need not worry much about objects being asynchronously freed as long as they check that the objects are still in the world model once after each call to spWMUpdate (e.g., with callbacks).
In the Java interface, garbage collection and finalize methods ensure that shared objects are not freed as long as any pointers to them remain. In the C interface the function spWMRegister is used to obtain a somewhat similar level of protection. This can be used to make sure that the object pointed to from a given variable will never be freed and therefore it will always be valid to check whether the object has been removed.
An important special case is worthy of note. If the local process owns an object, then it can rely on the fact that the object will not get removed unless the local process removes it. This obviates the need for a significant amount of spGetIsRemoved checking in typical applications.
If the local process has multiple threads, then several problems arise. First, the other threads in the process can remove anything and so no object access can be considered entirely safe. Second, it is likely that some of the threads will not be synchronized with calls on spWMUpdate. It is difficult for these threads to know when they should check that objects still exist.
The most robust way to avoid problems with threads is to avoid having asynchronous threads manipulate shared objects directly, but rather have them communicate in some other way with the main thread in a process. Barring this, spJustRemoved callbacks are the best way to ensure safety.
The extended Java syntax is supported by a preprocessor called
SPOT. In general, SPOT takes in a Java file containing a shared class
definition and produces:
In C-only mode, SPOT is used purely to extend the C API. In C-only
mode, everything in the definition of a class must be native. (What
this means for instance variables is discussed below.) In C-only mode
outputs (2) and (3) are unnecessary.
In Java-only mode, SPOT operates purely to extend the Java API. In
Java-only mode, classes can not contain any native elements. Things are
arranged so that the shared class can be used from Java without having
to link anything additional into the system core. In particular,
outputs (1) and (3) are unnecessary.
In mixed mode, the class can contain native and non-native elements and all
four outputs are produced so that the class can be used to maximum
effect in both C and Java. The shared classes described in this
document are defined solely using native elements and are processed
using the mixed mode of SPOT so that they are available fully in both the
Java and C APIs.
An appendix at the end of this documentation contains the SPOT input corresponding to the entire API described in this document.
The following example is used throughout the explanation
below. It is contrived to illustrate many different features in a
small space.
For the most part, the class definition above is standard Java.
The exceptions to this are the keyword `shared', the use of the
keyword `native' in an instance variable declaration, the use of the
syntax keyword/keyword, and the uses of the //* comments. (spThing
and spAction are shared object classes. spTransform and spDuration
are types in the C API.)
A class is a shared class if and only if it is the root
shared class sp or extends another shared class. (In the case above,
spExample extends the shared class spThing.) The key feature of a shared class is that the objects that are
created as instances of the class are shared between the processes
participating in a session.
The definition of a shared class must use the keyword `extends' and
can use the keywords `public' and/or `abstract'. However, it cannot
use any other keywords. In particular, it cannot use the keyword `implements'.
The open brace that begins the body of a shared class definition
can be followed by a comment beginning with `//*'. The text in the
comment can begin with [+/-SendToRegion +/-SendToContact]. This
specifies two key facts about the class: whether it is communicated in
the normal region-based way and whether it is communicated directly to
the contact point for a process, with `+' signifying yes and `-'
signifying no. If either the SendToRegion or the SendToContact
specification is omitted, it defaults to the value inherited from the
class that is being extended.
The SendToRegion and
SendToContact specifications are
used in a few crucial places in the definition of the standard shared
classes, but it is very unlikely that an application programmer would
ever need to use these flags. The reason for this is that for any
class that an application programmer is likely to define, the
inherited values of these flags will be correct.
The keyword `shared' specifies that an instance variable is shared
between processes. When a shared variable is set in one
process, the change is automatically reflected in all other processes.
In contrast, variables that are not marked as shared exist in
each process, but the values of non-shared variables are set
separately by each process, with no effect on their values in any
other processes.
The keyword `native' specifies that an instance variable is
represented in C so that efficient methods exist for accessing it
directly from C. (Shared implies native.)
The keyword native by itself is used often when defining the
standard classes in the system, and would be used by someone extending
the C API. It would never be used by someone who was solely
extending the Java API.
Shared and native instance variables cannot be
directly referred to as instance variables. (For instance, when using
an instance X of the class spExample, one cannot write X.Agent
anywhere.) Rather, a shared instance variable Var in the class spK is
accessed solely through the use of automatically generated
access methods.
Shared and native instance variables cannot redefine any variable in
the class being extended. (This is because, for efficiency, the
system assumes that none of the instance variables it uses can have
their definitions changed.)
If the keyword shared or native is used, one can also use the
access control keywords private, protected, and public. If an access
control keyword appears, then the Get and Set methods have the
specified access control. It is permissible to use the form
Keyword/Keyword, in which case the Get method has the access
limitations specified by the keyword before the / and the Set method
has the access limitations specified by the keyword after the /. (The
keyword before or after the / can be omitted with the meaning that the
corresponding accessor can be used anywhere in the package.) For
example, the Agent instance variable of spExample can be read by
everyone, but only written by the methods of the spExample class and
its subclasses.
If the keyword shared is used, it is not permissible to use the
keywords static or final. However, it is permissible for a native
variable to be static or static final.
If a native variable is specified to be static, it is processed
by SPOT in the same way as a variable that is not static except that
the methods created for accessing the variable are static. The only
class in the API that uses native static variables is the class
spWM.
If a native variable is specified to be static final, it acts as
a constant that is known both to Java and C.
It is referred to directly as a variable in both APIs, rather than
being accessed via methods. In C native static final
variables are If the keyword shared or native is used, then it is not
permissible to have an initialization expression, except for a
variable that is native static final. Rather, one uses an
Initialization method.
If the keyword shared or native is used, then the data stored in the
variable must be one of only a few permitted types. These types fall
into two groups: scalar types and pointer types.
A shared or native instance variable can have as its type any of
Java's eight primitive scalar data types (byte, short, int, long,
float, double, char, and boolean).
Since each shared and native instance variable is represented in C, it
must have a C type as well as a Java type. The following table shows
the default C type associated with each of the scalar Java types.
The semicolon ending a shared or native instance variable declaration
can be followed by a comment beginning with `//*' that specifies a
non-default type to use for the variable in the C API. This
specification has the form [type] where the type is a C type (defined
separately in a C file). The only restriction on the C type is that
it occupy the same amount of storage as the Java type. For instance,
an int in Java is represented using 32 bits. Any 32-bit C type can be
used in conjunction with it. Whenever they are passed as arguments to
functions or passed between C and Java, scalar values are copied.
In general, SPOT does nothing special with individual types. It
merely needs to know what types to use in Java and in C and what their
sizes are. However, one scalar type gets special treatment.
If the C type of a shared or native instance variable is spBoolean,
then the value is stored in a single bit using special internal variables of the class sp. However, once the bits
reserved in these variables have been exhausted, then a whole byte is
used for an spBoolean value.
In addition too the eight Java scalar types, a shared or native
instance variable can have one of the following pointer types.
As with scalar types, each of the pointer types above must be mapped
to a C type. The correspondence is shown in the table below.
In C, all shared objects are referred to by pointers of type sp and no
other C type can be specified. All functional arguments are referred
to by pointers of type spFn and no other C type can be specified.
Note that it is not possible to have a shared or native variable whose
Java type is anything other than the types specified above.
For strings and arrays, there is no default C type. Rather, the C
type must be specified using a `//*' comment. When specifying a pointer
type, this comment begins with [type:size] where type must be a C type
that is a pointer (e.g., float*) and where size specifies how many
elements there are in the string or array. For instance, in the
spExample class, the C type spTransform points to a vector of 17
floats.
When pointer types are passed as arguments to functions, they
are passed whenever possible by reference via a pointer. However,
when pointer types are passed between C and Java, the data is
typically copied so that appropriate data structures can be maintained
separately in C and Java.
As with scalar types, SPOT typically does nothing special with
individual pointer types. It merely needs to know what types to use
in Java and in C and what the memory size in C is. (For ease of
allocation and communication, strings and arrays stored in shared or
native variables are always stored in-line in the C representation
for an object.) However, several pointer types get special treatment.
The shared object types and the type spFn get special treatment so
that appropriate objects will be available efficiently in both Java
and C. In addition, Strings get special treatment.
A difference between Java and C is that Strings in Java use
Unicode characters while strings in C use ASCII characters. To
accommodate this, Java strings are encoded as null terminated UTF8
strings when communicated to C. (If a string only contains 7-bit
ASCII characters, this encoding leaves the bits unchanged.)
The C type spFixedAscii specifies a string of variable length
that can only be set at the moment when a shared object is being
created and cannot be changed later. To minimize the memory used for
spFixedAscii values, they are placed in a special variable-sized part
of an object. (A consequence of this is that it is not possible to
obtain the old value of an spFixedAscii variable.)
The type spFixedAscii can only be used for shared variables as
opposed to ones that are merely native. There can be at most one
spFixedAscii variable in a shared object. Unlike other array-like
types, no explicit size can be specified when using the type
spFixedAscii.
The total size (in C) of all the shared instance variables must fit in
a single UDP message (i.e., be less than 600 bytes or so). A warning
is issued if this restriction is violated.
The methods in the definition of a shared class are specified using
entirely standard Java code. However, for a method to be available in
C, it must be native, or redefine one of a few special methods that
the system core uses.
The specification of a native method in a shared class is
obligatorily followed by a //* comment containing the signature of the
C function that implements the method. The signatures are used by
SPOT both to create appropriate C .h files and to produce appropriate
stub files linking the Java and C API's.
In order to make it possible to create stubs correctly, types in
the C function signature that correspond to arrays
must obligatorily contain a specification of their
sizes as illustrated below. This is the same convention that is used
when specifying native or shared variables that contain arrays. In general, lengths must also be included for string types. However, it can be omitted if the string is null terminated.
For the generation of stubs, the names of the arguments are used
to determine the correspondence between the arguments of the Java and
C signatures. The Java and C type of an argument of a native method
must be one of the types that are valid for native variables. The
corresponding C type of the argument must be one of the C types that
are permitted to correspond to the Java type. Automatic conversions
are performed when passing values between Java and C.
If a method is not a static method, then the object the method is
applied to is passed to the C function via the first argument whose
name is not the same as the name of any argument in the Java method.
Otherwise, if an argument appears only in the C signature, it is
passed the value 0. If an argument appears only in the Java signature
it is not be passed to the C function.
In the standard API classes, the convention is followed that if a
class spK has a method M, then the name of the corresponding C
function is spKM, the arguments are in the same order, and the
argument to the C function that receives the object the method is
applied to is the first argument.
The following methods are interpreted in special ways. (Note that for
all these methods, there can be at most one method with the indicated
name in a given shared class definition.)
Initialization - As noted above, you cannot have initialization
expressions for native variables. However, you can define a method
called Initialization that initializes variables to any values you
want. (Note that unlike merely using initialization expressions, this
approach allows you to change the initialization of variables that are
inherited from an ancestor class.) If no Initialization method is
provided, SPOT defines one that does nothing.
New - You can define a static method called New that can be
called from C to create an instance of the class. This method can
take various arguments and initialize various values. If no New
method is provided, then SPOT creates one with no arguments that does
nothing other than call spClassNewObj, which in turn calls the
Initialization methods for the class and every ancestor class (calling
the ancestor methods first). (Operating in Mixed or Java-only mode
SPOT generates a creation method for each shared class which calls the
New method.)
C - A static method called C is automatically generated by SPOT. This
method returns the class object corresponding to the class being
defined. You cannot define a method with this name.
ReadData,
Function,
Predicate,
Inside - These four methods
are recorded in the class descriptor object (along with the
Initialization method) so that they can be called by the system core.
Accessor methods - In general, there is no
explicit mention in a shared class definition of the access methods
corresponding to shared and native variables because these methods are
generated by SPOT. However, If you include an explicit definition of
an accessor method, it will override what SPOT would have created. In
the definition of spExample above, this is the case for the method
SetTimeout.
Returning to the example above.
SPOT expects the following C functions to be defined separately:
Operating in C-only or mixed mode, SPOT generates the following functions:
SPOT would have generated spExampleNew, spExampleInitialization,
and spExampleSetTimeout if they had not been provided by the
programmer. The generated spExampleNew would just call
spClassNewObj with the class spExample as
its argument. The generated spExampleInitialization would do nothing.
The generated spExampleSetTimeout would merely set the value of the field.
A Java file input to SPOT can define non-shared classes
that are meaningful to Java and will be available in Java. These
classes cannot use the keyword shared. However, they can have native
static variables, native static final variables, and methods. These
three constructs are handled in exactly the same way as in shared
classes. In particular, the native variables are accessible in both C
and Java, and SPOT automatically creates stubs so that the native
methods are available in Java and C.
A Java file input to SPOT can define a non-shared class that does
not contain anything that is either native or shared. When that is
the case, the class has no relationship whatever to the system.
However, anything that is acceptable to Java can be used in the
definition of such a class.
A shared class can contain instance variables that are neither shared
nor native. Such a variable can be specified in any way that is
acceptable to Java. However, the variable will not be accessible from C.
In addition, a shared class can define non-native methods.
However, except for the methods Initialization, ReadData, Function,
Predicate, and Inside, non-native methods have nothing to do
with the system and will not be called by the system or available from C.
The central data structure in the API is the world model. There can be only one world model object at a time. It contains the various shared objects that are communicated among a group of processes. The spWM type does not extend the class sp and does not correspond to an object in the world model.
To start up a process, the first thing one does is create the world model to operate on. When the process wishes to terminate, it should remove the world model. In between, the world model is repetitively updated to reflect changes in the objects in the world model caused by other processes.
The class spWM defines the following instance variables:
The class spWM defines the following functions:
The following access functions are available:
Processes are identified by 32-bit session-wide unique owner ids of type spName that are assigned by the session manager. The Me variable of the spWM object contains
the owner id of the activity currently in control.
When the world model is created, the Me value is set to a main owner id assigned by the session manager. It can be changed at will later. Additional owner ids can be obtained using spWMGenerateOwner.
The following access functions are available:
The MainOwner variable of the spWM object contains the owner id assigned to the local process by the session manager. It is set when the world model is initially created and cannot be altered later.
The following access functions are available:
The Error instance variable of an spWM object records the most recent error to have occurred during the evaluation of any API function. When the world model is created the Error value is set to Null. It is changed whenever an error occurs. You can set the Error value back to Null if you want to. However, it cannot be directly set to any other value, but rather only indirectly via spWMReportError. Error strings are limited to being no more than 500 characters long.
The following access functions are available:
The LastError instance variable of an spWM object is identical to the Error instance variable except that it cannot be directly modified by an application, but rather only indirectly via spWMReportError.
The LastError value can always be consulted to determine what the most recent error, if any, was.
The following access functions are available:
The Interval instance variable of a world model records the time in milliseconds between the ends of the last two calls on spWMUpdate. When the world model is created, the interval is set to zero. After the second and subsequent calls on spWMUpdate, the interval value is updated to reflect the timing of events. It must not be modified by an application.
To be precise, the interval value is updated just before action processing begins and reflects the interval in time between corresponding points in spWMUpdate calls. Note particularly, that the interval is calculated after any waiting that spWMUpdate does in order to achieve the desired interval.
The following access functions are available:
The DesiredInterval instance variable of a world model controls the interval between calls on spWMUpdate as follows. When spWMUpdate is just about to begin processing actions, it determines the elapsed time since the last time actions were processed. If this time is less than the desired interval, then spWMUpdate waits until the interval is reached. If the elapsed time is greater than or equal to the desired interval, spWMUpdate proceeds without waiting. There is nothing that spWMUpdate can do to make processing take less time than it is taking. However, spWMUpdate ensures that the actual interval will not be less than the desired interval. Since there is a good deal of imprecision in the system's timing mechanisms, the system cannot guarantee that the actual interval will be equal to the desired interval; however, as long as the desired interval is long enough to be achievable, the system guarantees that the average error over time will be low.
When the world model is initially created, the desired interval is set to zero. This causes spWMUpdate to run as fast as possible without ever waiting. You can change the desired interval at any time.
It is important to think carefully about how often spWMUpdate
gets executed. If it executes too often, time is wasted looking at data
that has not changed in any useful way. Alternatively, If
spWMUpdate executes infrequently, you will be operating on
very stale data much of the time. Further, if spWMUpdate runs very infrequently (e.g., less than once
a second or so) some of the information about world model changes will
probably be lost as queues and buffers overflow. (Things are
arranged, however, so that even if you never call
spWMUpdate, the system will not crash.)
The following access functions are available:
The Window instance variable contains the current user interaction window. The exact type of the window object and the operations that can be applied to it depend on the operating environment---In Java they are one thing and in C under Windows95 they are another.
When the world model is first created, the Window is set to Null. The surrounding environment may force a particular value to be used subsequently. For instance, this is the case when using the system as a Netscape plugin. Otherwise, the application is free to create or choose a window to use.
Creates the world model. As part of this, establishes a connection to a session via a session server.
A process can only create one world model at a time.
The first argument is a DNS string that identifies the session server to connect to (e.g.,
A process must create the world model before using any shared object operation.
After initialization, the world model contains nothing but definitions of the built-in shared classes and a few internal objects that are not observable by applications.
The transfer vector is used to specify key internal operations (e.g., allocating memory) that may have special definitions that are required by the surrounding environment. In simple applications, Null is an acceptable value for this argument.
Eliminates the world model and disconnects the
process from the session it is in. Among other things, this causes all of the objects owned by the process to be removed from the world model and ensures that all the other processes in the session are informed of this fact. A process should always call spWMRemove before terminating. Once spWMRemove has been called, a process cannot use any shared object operation unless it first creates a new world model.
Causes the local world model copy to be updated to reflect changes made by other processes. The fundamental operation of an application process is based
on a cycle of: updating the world model (based on information
received from other processes); running the
application, waiting until the desired update interval has been reached, updating the world model again, and so on.
A number of important things happen each time
spWMUpdate is called.
(1) Other processes are notified of changes made in the local world
model copy only when spWMUpdate is called. This
gives the application control over when other processes see
changes. (For instance, one can ensure that several instance variables of an
object will change simultaneously).
(2) The world model is brought up to date by
processing messages from other processes. It is guaranteed
that the world model will not change in any way between calls to
spWMUpdate, unless the application makes the
changes itself.
(3) Any actions, including Callbacks, in the world model are run.
(This is the only time they are run.)
(4) As part of updating the world model, some objects may be removed
from the world model.
Activities in processes are identified by 32-bit session-wide unique ids of type spName. These owner ids are used to tag the objects created by different activities. The main owner id of a process is assigned by the session manager. Additional ids can be created by using the function spWMGenerateOwner. Each time this function is called it returns an additional owner id.
There are two kinds of owner ids: system and ordinary. spWMGenerateOwner returns ordinary owner ids. System owner ids are only used by the system core; there is no function in the API for creating them.
The purpose for having multiple owner ids in a single process is as the basis for controlling the visibility of objects via world model view masks.
The API utilizes a uniform approach to error reporting. This centers around the error reporting strings created by spWMReportError. Whenever an error occurs, spWMReportError is called and an error reporting string is stored so that it can be easily retrieved.
An error is created based on the data passed to spWMReportError as follows. The description string becomes the heart of the error report. It should be human readable, containing as much contextual information as possible. The system does not modify the descriptive string and does not retain a pointer to it.
The code is converted to ASCII and appended to the front of the description followed by a blank.
The code should be a unique identifier of the error.
(The codes for the errors reported by the system are all negative and every different error has its own unique error code. Applications are advised to use positive error codes.)
Programs that want to handle errors can tell which error occurred by looking at the error code portion of the report string.
The error string created is stored so that it can be retrieved as the value of spWMGetError and spWMGetLastError.
As an example of the way error reporting is used, consider that a careful program that was not completely sure that the variable X contained an spThing, might retrieve the Transform from X as follows:
After a shared object has been removed, the storage associated with it is eventually freed. However, if a pointer is registered using the function spWMRegister, then the system will never free an object that is pointed to by the pointer. This means that it will be safe to save an object in this pointer indefinitely. To stop protection, thereby allowing eventual freeing of the storage, call spWMDeregister.
The system essentially supports a restricted form of garbage collection where the function spWMRegister is used to specify exactly which pointers are taken into account for garbage collection purposes. Note that several threads can each register the same pointer and the pointer will be protected until after all the threads have deregistered it. Note also that it is pointers that are being protected, not objects per se.
Cancels the protection of a pointer started by spWMRegister. It is important to cancel registration when a pointer no longer needs to be protected. This is particularly true if the pointer is stack allocated and the function it is declared in is about to return, rendering the address of the pointer meaningless. Calling spWMRemove cancels all pointer registration.
An important part of the API is functions that map functional
arguments over objects. There are three basic kinds of mapping functions.
In C, spFn is the following functional type:
The following spFn could be used to count.
For instance, the following code counts the number of children of an object X.
Note that the state value that is passed to spExamineChildren and then on to the spFn spCount, is passed directly without copying. This is essential so that it can communicate information by side-effect. However, it means that the state must be in existance for the full period of time that spExamineChildren runs. This is not a problem for spExamineChildren, but requires more thought for operations like spExamineBeacons where the operate continues asynchronously for a potentially long period of time.
When an spFn is used as a callback predicate, the return value is interpreted as specifying whether an event has occurred. Specifically, the predicate should return True or
False depending on whether it observes that the event it
tests for has occurred.
Callback predicates define events in terms of changes to shared
instance variables. In particular, they compare the current state of
these variables with the state of these variables at the end of the
last call on spWMUpdate. (Since callback
predicates are evaluated during each call on
spWMUpdate, this ensures that any state change will
be detected.)
The following shows an example of a simple callback predicate.
There are several key things to note about callback predicates.
First, since callbacks are only applied to objects whose shared variables have changed, callback predicates are not called in situations where there
has been no change in the shared instance variables of an
object. Therefore, events must involve some change in these variables.
Second, the event generally must focus on changes in the shared instance
variables of an object. The API does not provide any way to look at
old values of local instance variables.
Third, callback predicates should not modify the object passed to them.
The API includes the following predefined callback predicates. Users can define any other predicates they want.
A fundamental feature of the API is the notion of a view mask for the
world model. The purpose of these masks is to control the visibility
of objects when using functions like spClassExamine.
The spMask type does not extend the class sp and does not correspond to objects in the shared world model. Rather, spMask data is stored in shared objects and used in intermediate computation.
The class spMask defines the following instance variables:
There are several kinds of objects that an application
typically should not see. First, they should not see objects that
exist purely for the use of the system core and are not part of the
external API. Second, they should not see objects that are created by
another activity and exist in the local world model copy only because
this activity happens to be running as part of the same process,
rather than in a separate process. (For instance, an application does
not want to see objects owned by other activities that are not
communicated to other processes listening in the regions the objects
are in.) Note that an application also typically does not want to see objects that have been removed. However, this is no problem because objects that have been removed are not in the local world model copy and therefore are not encountered by an application.
A world model view mask (spMask) is an integer bit mask that
controls what objects are viewable. A view mask is created by adding
(or or'ing together) the constants above.
Before discussing the exact meaning of these constants, it is
useful to consider a simple example. The following expression applies
F to every spThing object that is not the
private object of some other activity.
In contrast, the following expression applies F to every spThing
object that is owned by the current activity. It does not apply F to any objects owned by
other activities.
The following pseudo-code shows the reasoning applied in
functions like spExamineChildren to determine whether an
object is compatible with a view mask.
For a view mask to make any objects viewable, at least one of the
spMaskMINE and spMaskOTHERS bits must be on.
The three view masks constructible using spMaskMINE and
spMaskOTHERS that view any
objects are all of potential use to applications. The most common
single case is spMaskNORMAL which combines
spMaskMINE and spMaskOTHERS and views all
ordinary objects. This is the default
value for spMasks in most situations.
The functions spExamineChildren,
spExamineDescendants, spClassExamine, and
spClassMonitor all have view mask arguments that limit the
objects that are viewed. Actions and
callbacks also make use of view masks.
View mask restrictions do not apply to the access functions
(such as spGetParent) for obtaining the value of instance
variables. This is not a problem because, in general, you cannot get
from objects you should see to ones you should not see. Objects with ordinary
owners should never point to objects with system owners. Objects that
are communicated via regions never point to ones that are not. Note that no object
ever points to an object that has been removed.
As illustrated above, view masks can be created by adding or or'ing these constants together.
The data type spTransform is used as the fundamental representation of the position, orientation, and scaling of objects. A key advantage of an spTransform is that the various components are easy to understand. The components are also well suited to interpolation. The spTransform type does not extend the class sp and does not correspond to objects in the shared world model. Rather, spTransform data is stored in shared objects and used in intermediate computation.
The class spTransform defines the following instance variables:
The class spTransform defines the following functions:
An spTransform contains the same information as in a VRML transform node. Specifically, an spTransform is a vector of 17 floats representing 5 logical values as follows.
The first three elements are a vector representing the translation. The next four elements are an spRotation vector representing a rotation. The next three elements specify the amount of scaling along the X, Y, and Z axes with the value 1.0 indicating no scaling. The next four elements are an spRotation that is applied before the scaling is performed. The final three elements specify a center point that specifies the center point for both rotations.
As in VRML, the parts of an spTransform act together as follows.
Note that because of the ScaleOrientation, the scale value can specify
shear as well as scaling. It can be shown that the composition of any two spTransforms can be represented as an spTransform and the inverse of any spTransform can be represented as an spTransform.
By convention, Spline uses a right-hand coordinate system with the
Y axis up and objects facing down the negative Z axis. (No assumptions are made about the relationship of the X and Y axes to compass directions.) It should be noted that
different graphics modeling languages disagree with each other
about these conventions. For instance, some have the Z axis up. The way spVisualDefinition links are
specified makes it easy to use any kind of graphic model, without
having to modify it.
The origin of the coordinate system for an object should be in
the middle of the object unless there is a compelling reason
otherwise. This is needed so that the InRadius and OutRadius of
spThings will be meaningful. (An example of a compelling reason why
the origin of an object would not be in the middle is that the origin
of a subpart of an articulated form should be at the pivot point.
This makes the mathematics of moving the subpart much easier.)
Any object that has a recognizable up direction should be
oriented so that this up direction is parallel to the Y axis and
points toward positive Y. Similarly, any object that has a
recognizable front should have the front facing toward the negative Z
axis. For instance, an object corresponding to the torso of an avatar
would have the origin of its coordinate system in the middle of the
chest, with the Y axis pointing up toward the head and the negative Z axis
pointing out through the front of the torso. These axis conventions
are needed both so that it is easy to combine different objects into a
single scene and so that simulations that want to interact with
objects can find where the tops and fronts of the objects are.
All units are in terms of meters and radians. Radians are used because they are more natural for numerical calculations. However, most people think more easily in degrees. In recognition of this, the following constant is provided.
Using this constant you can specify PI/2 radians (90 degrees) as follows.
In C, an spTransform is the following type.
In addition, the following type is available for allocating
memory for an spTransform. When calling a function that operates on
spTransforms, one can pass in a variable that is either of the type
spTransform or spTransformData.
Second, you can use the functions described in the following subsections
to operate on spTransform objects. These make it easier to do
complex operations. However, they
require a clear understanding of transform interactions in
order to figure out how to obtain a desired effect. In
addition, these operations are static in the sense they call for the
computation of particular spTransforms, rather than sequences of transform
values over time.
Several conventions are worthy of note about the functions on
spTransform objects.
None of the functions ever allocates memory.
Rather, the control of memory is left entirely up to the application.
All modifications are by side-effect. However, to make it easier to create nested expressions the modified value is used as the return value.
A key complexity is that four different representations for
rotations are supported. This is necessary because different
communities of people are accustomed to different representations and
justified because each representation has a situation where it is
particularly convenient. The primary rotation representation is spRotation vectors such as those included in an spTransform. The second rotation representation is a
vector of 3 Euler angles (in an spVector). The Third rotation
representation is a vector of 4 floats representing a quaternion (spQuaternion). The fourth
rotation representation is the
rotation part of an spMatrix.
For example, you might write.
Copies the data from a Source spTransform to another, which is returned.
Initializes the values in an spTransform so that they specify no translation, rotation, or scaling. That is to say, the spTransform is set to (0,0,0, 0,0,1,0, 1,1,1, 0,0,1,0, 0,0,0).
It is important to initialize an spTransform before using it as a source of data, because the operations on spTransforms do not test that transforms are well formed before beginning their operations. In particular, they assume that the rotations in it are well formed.
Returns the Translation portion of an spTransform. In C, the spVector returned shares memory with the spTransform.
Sets the Translation portion of an spTransform. The other parts of the spTransform are not altered.
Returns the Rotation portion of an spTransform represented as an spRotation.
In C, the spRotation returned shares memory with the spTransform.
Sets the Rotation portion of an spTransform. The other parts of the spTransform are not altered.
Returns the Scale portion of an spTransform. In C, the spVector returned shares memory with the spTransform.
The three elements of the spVector contain the amount of scale along the X, Y, and Z axes respectively. The value 1.0 indicates no change in scale.
Sets the Scale portion of an spTransform. The other parts of the spTransform are not altered.
Returns the ScaleOrientation portion of an spTransform represented as an spRotation.
In C, the spRotation returned shares memory with the spTransform.
Sets the ScaleOrientation portion of an spTransform. The other parts of the spTransform are not altered.
Returns the Center portion of an spTransform.
In C, the spVector returned shares memory with the spTransform.
Sets the Center portion of an spTransform. The other parts of the spTransform are not altered.
The type spVector is a 3-element vector of floats. It is used to
represent several distinct things. The spVector type does not extend the class sp and does not correspond to objects in the shared world model. Rather, spVector data is stored in shared objects and used in intermediate computation.
The class spVector defines the following instance variables:
The class spVector defines the following functions:
There are three major uses of vectors. The first is as an
ordinary vector in Cartesian coordinates, with the three elements
being the X, Y, and Z coordinates respectively. These vectors are
used to specify translations, rotation axes, and center points.
The second use of vectors is as a set of Euler angles. In this
use, the first element is a rotation around the X axis in radians;
the second element is a rotation around the Y axis in radians; and
the third element is a rotation around the Z axis in radians. Since
rotations do not commute, it is important to realize that these
correspond to first rotating around Z, and then rotating around Y,
and lastly rotating around X.
The third use of vectors is as a representation for scaling. In
this use, the three elements represent the amount of scaling in the X,
Y, and Z axes respectively. A value of 1.0 in an element specifies no
scaling.
In C, an spVector is the following type.
In addition, the following type is available for stack allocating
memory for an spVector. When calling a function that operates on
spVectors, one can pass in a variable that is either of the type
spVector or spVectorData.
Several conventions are worthy of note about the functions on
spVectors.
None of the functions ever allocates memory.
Rather, the control of memory is left entirely up to the application.
All modifications are by side-effect. However, to make it easier to create nested expressions the modified vector is used as the return value.
For example, you might write.
The following four constants contain particular vectors that are useful as arguments to various API functions. (In C, these constants are external variables initialized to appropriate values.)
For example, you might write.
Copies a vector into another.
Sets all three elements of an spVector to be equal to a given Scalar.
Returns True if two vectors are component-by-component equal within a small system-defined tolerance.
Returns True if two vectors are component-by-component equal within a tolerance specified by the user.
Adds two vectors together and stores the result in the first vector.
Subtracts a second vector from a first vector and stores the result in the first vector.
Modifies a vector by multiplying each element by a scalar.
Modifies a vector by dividing each element by a scalar.
Computes the cross product of two vectors and stores it in the first vector.
Computes the dot product of two vectors and stores the result in the first vector.
Modifies an spVector by multiplying each element by the corresponding element of another spVector.
Computes the length of a vector. That is to say, the square root of the sum of the squares of the elements.
Converts a vector into a unit length vector pointing in the same direction. That is to say, divides each element by the length of the vector.
The principle way that rotations are specified is in terms of a rotation axis passing through the origin and a rotation angle about this axis in radians. Typically, the rotation angle is between plus or minus PI. A key advantage of an spRotation is that the various components are easy to understand. In addition, because the components are relatively independent, they are well suited to interpolation. The spRotation type does not extend the class sp and does not correspond to objects in the shared world model. Rather, spRotation data is stored in shared objects and used in intermediate computation.
The class spRotation defines the following instance variables:
The class spRotation defines the following functions:
An spRotation is a vector of 4 floats consisting of two parts as follows.
The first three elements are a vector representing an axis through the origin to rotate about. The last element is an amount of rotation in radians.
In C, an spRotation is the following type.
In addition, the following type is available for allocating
memory for an spRotation. When calling a function that operates on
spRotations, one can pass in a variable that is either of the type
spRotation or spRotationData.
Several conventions are worthy of note about the functions on
spRotations.
None of the functions ever allocates memory.
Rather, the control of memory is left entirely up to the application.
All modifications are by side-effect. However, to make it easier to create nested expressions the modified value is used as the return value.
For example, you might write.
Copies the data from a Source spRotation to another, which is returned.
Initializes the values in an spRotation so that they specify no rotation about the Z axis. That is to say, the spRotation is set to (0,0,1,0).
It is important to initialize an spRotation before using it as a source of data because the operations on spRotations do not test that rotations are well formed before beginning their operations. In particular, they assume that the axis does not have zero length.
Returns the Axis portion of an spRotation. In C, the spVector returned shares memory with the spRotation.
Sets the Axis portion of an spTransform. The angle is not altered.
Returns the Angle in an spRotation.
Sets the Angle in an spTransform. The axis is not altered.
Computes the spQuaternion corresponding to an spRotation.
Computes the spRotation corresponding to an spQuaternion.
Computes the Euler angles corresponding to an spRotation.
1.2 System Overview
Above all else, the system provides a convenient architecture for
implementing multi-user interactive environments. This architecture
is centered on a world model that mediates all interaction.
Figure 1 illustrates the application programming model. It shows five
applications interacting through the world model.
1.2.1 Scalability
Application programmers are encouraged to think in terms of Figure 1.
However, it would not work well to use a centralized architecture when
actually implementing the system. Rather, the system operates as
shown in Figure 2. To provide low latency interaction with the world
model, the world model is replicated so that a copy resides in each
application process. Messages sent over a computer network linking
the processes are used to propagate changes from one world model copy
to another.
1.2.2 Servers
A significant feature of Figure 2 is that it does not contain a
central process. To minimize latency, and prevent bottlenecks, the
primary communication used by the system is peer-to-peer rather than passing through
centralized processes. However, centralized server processes are used for
five key services. The way these servers interact with applications
is illustrated in Figure 3.
1.2.3 Application Processes
The structure of a single application process is shown in Figure 4.
The dashed box at the top of the figure shows how the application
itself fits into the picture.
1.2.4 The API
The Application Program Interface (API) described in this document
consists primarily of operations for creating/deleting objects in the
world model and reading/writing instance variables in these objects.
The application support module (see Figure 4) contains various facilities that make
application writing easier. For the convenience of application
writers, multiple APIs are provided. The principle APIs being in ANSI
C and Java.
1.2.5 Rendering
Figure 5 shows the system being used to support an application that
interacts with a human user. The primary feature of the figure is
that three applications are used in this situation. The main
application (in the dashed box) presents an interface to the user.
1.2.6 Supporting Simulations
From the earliest days of work on the system, we paid close
attention to supporting interaction with computer simulations as well
as people. The way this is done is shown in Figure 6.
1.3 Atomic Data
Much of the data used in the API is simple atomic data of 64
bits or less in length such as boolean values, integers, and floating
point numbers. This data is copied whenever it is passed to or
returned from a function.
1.4 Pointer Data
In addition to atomic data, the API makes use of several different
kinds of pointer data. As discussed at length in the next section,
the most important kind of pointer data is pointers to shared objects.
However, several other kinds of pointer data are used as well.
1.5 Shared Objects
The central kind of data in the API consists of shared objects. These objects are stored in a world model and shared between the participants in a session. (Other data is shared only if it is stored in a shared variable of a shared object.) The key feature of shared objects is instance variables. They can have methods associated with them as well; however, the system makes relatively little use of these methods. Rather, shared objects are essentially passive, principally just representing a database of information.
1.5.1 Accessors
T spKGetV(sp Object)
void spKSetV(sp Object, T Value)
T spKGetOldV(sp Object)
1.5.2 Referring To
Instance variables that point to shared objects are handled specially in a number of ways. This is necessary due to the partial and asynchronous way that the world model is copied between processes.
1.5.3 Removal
A key issue is asynchronous changes in the world model. In particular, objects owned by other processes can appear, change, and disappear from the world model any time spWMUpdate is called. In addition, if there are multiple threads in the local process, then from the perspective of a given thread, other threads can asynchronously create, modify, and remove locally owned objects.
1.6 Defining Shared Classes
No matter what API language is being used, shared objects are defined
using Java with a few small extensions. (The following discussion
assumes a basic understanding of Java.)
SPOT operates in 3 basic modes: C-only, Java-only, and mixed.
1.6.1 Example
public class spExample extends spThing { //* [+SendToContact]
shared public float[] Orientation; //* [spTransform:17]
shared public/protected spAction Agent;
native int Timeout; //* [spDuration]
native public static int New(float [] Orientation);
//* [sp spExampleNew(spTransform:17 Orientation)]
native public void Initialization();
//* [void spExampleInitialization(sp Object)]
native void SetTimeout(int Timeout);
//* [void spExampleSetTimeout(sp Object, spDuration Timeout)]
native public final void Setup(int Timeout, spAction Action);
//* [void spExampleSetup(sp ExampleObj, spDuration Timeout, sp Action)]
}
1.6.2 Shared Classes
1.6.3 Native Instance Variables
#defines. An example of a native static final
variable in the API is spDEGREES.
1.6.4 Scalar Types
1.6.5 Pointer Types
1.6.6 Native Static Final Variables
A shared or native variable has an initializer expression if and only
if it is a native static final variable. If the initialization of the
value in C needs to be different from the initialization in Java
(e.g., because it is not just a numeric constant) then the C
initialization can be specified after an `=' sign in the //* comment
that specifies the C type of the variable. For example the definition
of spDEGREES could be:
native static final public float DEGREES = Math.PI/180.0; //* [float=M_PI/180.0]
1.6.7 Methods
native public static int New(float [] Orientation);
//* [sp spExampleNew(spTransform:17 Orientation)]
1.6.8 Example Revisited
public class spExample extends spThing { //* [+SendToContact]
shared public float[] Orientation; //* [spTransform:17]
shared public/protected spAction Agent;
native int Timeout; //* [spDuration]
native public static int New(float [] Orientation);
//* [sp spExampleNew(spTransform Orientation)]
native public void Initialization();
//* [void spExampleInitialization(sp Object)]
native void SetTimeout(int Timeout);
//* [void spExampleSetTimeout(sp Object, spDuration Timeout)]
native public final void Setup(int Timeout, spAction Action);
//* [void spExampleSetup(sp ExampleObj, spDuration Timeout, sp Action)]
}
extern sp spExampleNew(spTransform Orientation);
extern void spExampleInitialization(sp Object);
extern void spExampleSetTimeout(sp Object, spDuration Timeout);
extern void spExampleSetup(sp ExampleObj, spDuration Timeout, sp Action);
extern sp spExampleC();
extern spTransform spExampleGetOrientation(sp Object);
extern spTransform spExampleGetOldOrientation(sp Object);
extern void spExampleSetOrientation(sp Object, spTransform Orientation);
extern sp spExampleGetAgent(sp Object);
extern sp spExampleGetOldAgent(sp Object);
extern void spExampleSetAgent(sp Object, sp Agent);
extern spDuration spExampleGetTimeout(sp Object);
1.6.8 Non-Shared classes
2 spWM
2.1 Me
native public static int Me; //* [spName]
spName spWMGetMe()
void spWMSetMe(spName X)
2.2 MainOwner
native public/ static int MainOwner; //* [spName]
spName spWMGetMainOwner()
2.3 Error
native public static String Error; //* [char *:500]
char * spWMGetError()
void spWMSetError(char * X)
2.4 LastError
native public/ static String LastError; //* [char *:500]
char * spWMGetLastError()
2.5 Interval
native public/ static spDuration Interval; //* [spDuration]
spDuration spWMGetInterval()
2.6 DesiredInterval
native public static spDuration DesiredInterval; //* [spDuration]
spDuration spWMGetDesiredInterval()
void spWMSetDesiredInterval(spDuration X)
2.7 Window
native public static int Window; //* [spWindow]
spWindow spWMGetWindow()
void spWMSetWindow(spWindow X)
2.8 spWMNew
spWM spWMNew(char * Server, spTransferVector V)
"node.myUniv.edu").
If the server address string is Null, then all phases of initialization are performed except contacting the session server. This is useful for some kinds of single-user testing.
2.9 spWMRemove
void spWMRemove()
2.10 spWMUpdate
void spWMUpdate()
2.11 spWMGenerateOwner
spName spWMGenerateOwner()
2.12 spWMReportError
void spWMReportError(sp Object, long Code, char * Description)
spWMSetError(NULL);
transform = spThingGetTransform(X);
if (spWMGetError()) ...
2.13 spWMRegister
void spWMRegister(sp * Pointer)
2.14 spWMDeregister
void spWMDeregister(sp * Pointer)
3 spFn
The type spFn is used for the functional arguments to
all the above functions. (A single type is used in all these situations
because this is considerably more convenient than having to
define a separate type for each purpose.)
The spFn type does not extend the class sp and does not correspond to objects in the shared world model. Rather, spFn data is stored in shared objects and used in intermediate computation.
typedef spBoolean spFn(sp Object, void * State)
The State argument is used to communicate state
information between calls to an spFn. It can be used to accumulate a
result. The return value is used for search-like operations. If an
spFn ever returns True, then the mapping activity
immediately halts. It is essential that an spFn be light weight in the sense that it runs quickly,
and must not call spWMUpdate.
spBoolean spCount(sp ignore, void * state) {
int * count = (int *)state;
*count = *count+1;
return FALSE;
}
int Counter = 0;
spExamineChildren(X, spMaskNORMAL, spCount, &Counter);
Result = Counter;
The value of the count is obtained by observing the value of the
state variable Counter after the examination is over.
3.1 spFn Predicates
Another key part of the API is the ability to install callback
functions that will automatically be called when certain events occur.
Events are defined by predicates that test changes in the shared
variables of objects. These predicates are defined using the same
type spFn as functions that are mapped over objects.
spBoolean spChangedParent(sp object, void * ignore) {
return spThingGetOldParent(object) != spThingGetParent(object);
}
4 spMask
spClassExamine(spThingC(), spMaskNORMAL, F, NULL)
spClassExamine(spThingC(), spMaskMINE, F, NULL)
boolean CompatibleWithMask(sp object, int mask) {
if (spGetOwner(object) == spWMGetMe()) {
return (spMaskMINE & mask)
}
else {
return (spMaskOTHERS & mask) &&
(~ SystemOwner(spGetOwner(object))) &&
( spClassGetSendToRegion(spGetClass(object)))
}
}
4.1 Constants
View masks are created by combining the following
constants. (In C these constants are
#defines.)
5 spTransform
(translate by Translation
(translate by Center
(rotate by Rotation
(rotate by ScaleOrientation
(scale by Scale
(rotate by -ScaleOrientation
(translate by -Center
...)))))))
90.0f * spDEGREES
typedef float * spTransform;
typedef float spTransformData[17];
There are several levels at which you can interact with an
spTransform. First, you can alter the underlying vector directly,
e.g., using the index constants above. This allows you to do
everything that is possible, but nothing easily.
5.1 Constants
The following constants are available for directly accessing the
various elements of an spTransform. (In C, these constants are
#defines.)
spTransform P;
P[spTransformX] = P[spTransformY] + 2.0;
5.2 spTransformCopy
spTransform spTransformCopy(spTransform Destination, spTransform Source)
5.3 spTransformFromIdent
spTransform spTransformFromIdent(spTransform Transform)
5.4 spTransformGetTranslation
spVector spTransformGetTranslation(spTransform Transform)
5.5 spTransformSetTranslation
spTransform spTransformSetTranslation(spTransform Transform, spVector Translation)
5.6 spTransformGetRotation
spRotation spTransformGetRotation(spTransform Transform)
5.7 spTransformSetRotation
spTransform spTransformSetRotation(spTransform Transform, spRotation Rotation)
5.8 spTransformGetScale
spVector spTransformGetScale(spTransform Transform)
5.9 spTransformSetScale
spTransform spTransformSetScale(spTransform Transform, spVector Vector)
5.10 spTransformGetScaleOrientation
spRotation spTransformGetScaleOrientation(spTransform Transform)
5.11 spTransformSetScaleOrientation
spTransform spTransformSetScaleOrientation(spTransform Transform, spRotation R)
5.12 spTransformGetCenter
spVector spTransformGetCenter(spTransform Transform)
5.13 spTransformSetCenter
spTransform spTransformSetCenter(spTransform Transform, spVector Center)
6 spVector
typedef float * spVector;
typedef float spVectorData[3];
6.1 Constants
The following three constants are provided for accessing the components of an spVector. (In C, these constants are external variables initialized to appropriate values.)
spVector V;
V[spVectorX] = P[spVectorY] + 2.0;
spRotation R;
spRotationLookAt(R, spVectorZERO, spVectorAXISX, spVectorAXISY);
6.2 spVectorCopy
spVector spVectorCopy(spVector Destination, spVector Source)
6.3 spVectorSetFromScalar
spVector spVectorSetFromScalar(spVector Vector, float Scalar)
6.4 spVectorEquals
spBoolean spVectorEquals(spVector A, spVector B)
6.5 spVectorEqualsDelta
spBoolean spVectorEqualsDelta(spVector A, spVector B, float Tolerance)
6.6 spVectorAdd
spVector spVectorAdd(spVector A, spVector B)
6.7 spVectorSubtract
spVector spVectorSubtract(spVector A, spVector B)
6.8 spVectorMultiplyByScalar
spVector spVectorMultiplyByScalar(spVector Vector, float Scalar)
6.9 spVectorDivideByScalar
spVector spVectorDivideByScalar(spVector Vector, float Scalar)
6.10 spVectorCrossProduct
spVector spVectorCrossProduct(spVector A, spVector B)
6.11 spVectorDotProduct
float spVectorDotProduct(spVector A, spVector B)
6.12 spVectorComposeScales
spVector spVectorComposeScales(spVector A, spVector B)
6.13 spVectorLength
float spVectorLength(spVector Vector)
6.14 spVectorNormalize
spVector spVectorNormalize(spVector Vector)
7 spRotation
typedef float * spRotation;
typedef float spRotationData[4];
7.1 Constants
The class spRotation includes the following constants for accessing the X, Y, Z, and Angle
components of an spRotation. (In C, these constants are
#defines.)
spRotation R;
R[spRotationX] = A[spRotationY] + 2.0;
7.2 spRotationCopy
spRotation spRotationCopy(spRotation Destination, spRotation Source)
7.3 spRotationFromIdent
spRotation spRotationFromIdent(spRotation Rotation)
7.4 spRotationGetAxis
spVector spRotationGetAxis(spRotation Rotation)
7.5 spRotationSetAxis
spRotation spRotationSetAxis(spRotation Rotation, spVector Axis)
7.6 spRotationGetAngle
float spRotationGetAngle(spRotation Rotation)
7.7 spRotationSetAngle
spRotation spRotationSetAngle(spRotation Rotation, float Angle)
7.8 spRotationToQuat
spQuaternion spRotationToQuat(spRotation Rotation, spQuaternion Quat)
7.9 spRotationFromQuat
spRotation spRotationFromQuat(spRotation Rotation, spQuaternion Quat)
7.10 spRotationToAngles
spVector spRotationToAngles(spRotation Rotation, spVector Vector)
7.11 spRotationFromAngles
spRotation spRotationFromAngles(spRotation Rotation, spVector Angles)