User Tools

Site Tools


keckcaves:pyvrui_release_notes_-_10-july-2009

pyVrui Release Notes - 10-July-2009

Much has changed in the pyVrui interface. A summary follows. Skip to the end to "Porting Notes For Previous Versions" for a summary of things to change in existing code.

Simplified Interface to Vrui Application Objects

There is a new class for defining the arguments for a Vrui application: Vrui.AppStringArray:

      class AppStringArray(pyApplicationArgsStringArray):
          def __init__(self, *initial_items):
              """
              if any initial_items are given, they must all be strings.
              """
              pyApplicationArgsStringArray.__init__(self)
              self.extend(initial_items)
          def __repr__(self):
              return str(list(self))
          def withExtendingItems(self, *items):
              """
              extends and then returns self.
              if any items are given, they must all be strings.
              """
              self.extend(items)
              return self

This class is still underlyingly std::vector<std::string>, i.e., a collection of strings, but is much easier to work with. For example:

      Vrui.AppStringArray() ==> []
      Vrui.AppStringArray('a', 'b', 'c') ==> ['a', 'b', 'c']
      Vrui.AppStringArray('a', 'b', 'c').withExtendingItems('d', 'e', 'f') ==> ['a', 'b', 'c', 'd', 'e', 'f']

– There is also a new class derived from Vrui.pyApplication: Vrui.App. This incorporates Chris's suggestion regarding making sys.argv the default argument list, plus incorporates the interactive mode threading stuff.

      class App(pyApplication):
          def __init__(self, args=None, defaults=None, interactive=False):
              """
              args == None ==> use args initialized to sys.argv
              defaults == None ==> use empty defaults
              interactive: if True, launch master node run() method in a secondary thread,
              permitting app to be run interactively like: ipython -i appscript.py
              """
              if args is None:
                  args = sys.argv
              if defaults is None:
                  defaults = []
              pyApplication.__init__(self, AppStringArray(*args), AppStringArray(*defaults))
              self.__interactive = interactive
          def start(self):
              if self.__interactive and Vrui.isMaster():
                  threading.Thread(target=self.run).start()
              else:
                  self.run()

To use the new Vrui.App, derive your application class from Vrui.App, initialize Vrui.App in your class's init, and call start() instead of run(). These steps are explained in more detail in the following:

First derive your application from Vrui.App instead of Vrui.pyApplication (and Vrui.GLObject as usual):

      class DemoApp(Vrui.App, Vrui.GLObject):

Then, in the init for the application:

          def __init__(self, *args, **kwargs):
              Vrui.App.__init__(self, *args, **kwargs)
              Vrui.GLObject.__init__(self)
              # ... other initialization for your application ...

Vrui.App's constructor takes two optional arguments and one optional keyword argument:

      args .......... None or a Vrui.AppStringArray instance representing the command line arguments, None ==> sys.argv
      defaults ...... None or a Vrui.AppStringArray instance representing the Vrui appDefaults, None ==> empty
      interactive ... (keyword) True if the master node's Vrui.App.run() method should be started in a secondary thread

To use the interactive feature, you must call start() instead of run() on your application object. start() will then call run().

Vrui.App in conjunction with Vrui.AppStringArray permits streamlined initialization of the application as in the following four examples:

      1. Interactive startup with sys.argv as args (should be invoked as: ipython -i appscript.py):
          if __name__ == '__main__':
              app = DemoApp(interactive=True)  # save application object for interactive use
              app.start()
      2. Noninteractive startup with sys.argv as args:
          if __name__ == '__main__':
              DemoApp().start()
      3. Calling run() instead of start() also works for noninteractive mode:
          if __name__ == '__main__':
              DemoApp().run()  # standard Vrui startup with run() instead of start(); noninteractive
      4. Startup with custom arguments:
          if __name__ == '__main__':
              DemoApp(['my', 'custom', 'args']).start()

Note: Vrui parses the command line in Vrui.Application.init and removes the arguments it recognizes and uses (e.g., -vislet arguments). The updated versions of args and defaults are available as properties of application instance as in app.args and app.defaults.

Static and Free Functions Exposed (More Appropriately) as Methods

Due to complications in defining actual methods in the Boost.Python C/C++ interface, many manufactured methods (like SetContained(), described later) are exposed as static/class methods or as free functions. However, in the Vrui package init.py many of these are now also exposed as actual methods making their usage much more natural. These methods are summarized below.

– Contained / Uncontained / SetContained: The following classes now have a setContained method callable as instance.setContained(isContained):

      Vrui.ALObject.DataItem
      Vrui.GLObject.DataItem
      Vrui.Blind
      Vrui.Button
      Vrui.CascadeButton
      Vrui.Container
      Vrui.DecoratedButton
      Vrui.DragWidget
      Vrui.DropdownBox
      Vrui.Label
      Vrui.ListBox
      Vrui.Margin
      Vrui.Menu
      Vrui.PopupMenu
      Vrui.PopupWindow
      Vrui.RadioBox
      Vrui.RowColumn
      Vrui.ScrollBar
      Vrui.ScrolledListBox
      Vrui.Separator
      Vrui.SingleChildContainer
      Vrui.Slider
      Vrui.SubMenu
      Vrui.TextField
      Vrui.TitleBar
      Vrui.ToggleButton
      Vrui.Widget

– Vrui.MulticastPipe now has methods the following additional methods:

      pipe.writeUInt8(value)   is equivalent to:  Vrui.MulticastPipe.WriteUInt8(pipe, value)
      pipe.readUInt8()         is equivalent to:  Vrui.MulticastPipe.ReadUInt8(pipe)
      pipe.writeUInt16(value)  is equivalent to:  Vrui.MulticastPipe.WriteUInt16(pipe, value)
      pipe.readUInt16()        is equivalent to:  Vrui.MulticastPipe.ReadUInt16(pipe)
      pipe.writeUInt32(value)  is equivalent to:  Vrui.MulticastPipe.WriteUInt32(pipe, value)
      pipe.readUInt32()        is equivalent to:  Vrui.MulticastPipe.ReadUInt32(pipe)
      pipe.writeFloat(value)   is equivalent to:  Vrui.MulticastPipe.WriteFloat(pipe, value)
      pipe.readFloat()         is equivalent to:  Vrui.MulticastPipe.ReadFloat(pipe)
      pipe.writeDouble(value)  is equivalent to:  Vrui.MulticastPipe.WriteDouble(pipe, value)
      pipe.readDouble()        is equivalent to:  Vrui.MulticastPipe.ReadDouble(pipe)
      pipe.writeString(value)  is equivalent to:  Vrui.MulticastPipe.WriteString(pipe, value)
      pipe.readString()        is equivalent to:  Vrui.MulticastPipe.ReadString(pipe)

– Vrui.CallbackList previously had class methods add(), addToFront() and remove(). These have been changed to, respectively, Add(), AddToFront() and Remove() and the former (“lowercase”) names now name methods:

      callbackList.add(callback)         is equivalent to:  Vrui.CallbackList.Add(callbackList, callback)
      callbackList.addToFront(callback)  is equivalent to:  Vrui.CallbackList.AddToFront(callbackList, callback)
      callbackList.remove(callback)      is equivalent to:  Vrui.CallbackList.Remove(callbackList, callback)

– Previously, a free function RetrieveDataItem(glContextData, thing) was exposed to retrieve objects stored via Vrui.GLContextData.addDataItem(). However, the similar Vrui.ALContextData.addDataItem() was not exposed. To remedy this, there are now two free functions RetrieveALDataItem() and RetrieveGLDataItem(). RetrieveDataItem() will now take either a Vrui.ALContextData or Vrui.GLContextData context object.

In addition, two methods are now exposed:

      alContext.retrieveDataItem(thing)  is equivalent to:  RetrieveALDataItem(alContext, thing)
      glContext.retrieveDataItem(thing)  is equivalent to:  RetrieveGLDataItem(glContext, thing)

and:

      RetrieveDataItem(alContext, thing)  is equivalent to:  RetrieveALDataItem(alContext, thing)
      RetrieveDataItem(glContext, thing)  is equivalent to:  RetrieveGLDataItem(glContext, thing)

and:

      RetrieveDataItem(anythingElse, thing)  raises a TypeError

– You might be wondering why all these methods are necessary in the first place. It is because their analogs in C++ Vrui are funciton templates that cannot be directly exposed to Python. So, in the case of Vrui.MulticastPipe for example, we expose a few specific instantiations with useful types.

Contained Object Management

Contained objects are those whose base class is a C++ class and may be stored in C++ containers that expect to take ownership of those contained objects, i.e., the containers will delete the contained objects when done with them. This is in contrast with a normal Python-wrapped C++ class instance where the Python layer will delete the C++ object when the last reference to the wrapping Python object disappears. This can be disastrous when a Python object wrapping a C++ object stored in a C++ data structure goes away and deletes the C++ object along with it.

To solve this problem, PyVrui instruments certain objects as “contained objects”:

      Vrui.ALObject.DataItem
      Vrui.GLObject.DataItem
      Vrui.Blind
      Vrui.Button
      Vrui.CascadeButton
      Vrui.Container
      Vrui.DecoratedButton
      Vrui.DragWidget
      Vrui.DropdownBox
      Vrui.Label
      Vrui.ListBox
      Vrui.Margin
      Vrui.Menu
      Vrui.PopupMenu
      Vrui.PopupWindow
      Vrui.RadioBox
      Vrui.RowColumn
      Vrui.ScrollBar
      Vrui.ScrolledListBox
      Vrui.Separator
      Vrui.SingleChildContainer
      Vrui.Slider
      Vrui.SubMenu
      Vrui.TextField
      Vrui.TitleBar
      Vrui.ToggleButton
      Vrui.Widget

and also instruments the ownership-taking methods of certain “container” objects:

      Vrui.ALContextData        | addDataItem()
      Vrui.GLContextData        | addDataItem()
      Vrui.Container            | addChild()
      Vrui.Menu                 | addChild()
      Vrui.PopupWindow          | addChild()
      Vrui.RadioBox             | addChild()
      Vrui.RowColumn            | addChild()
      Vrui.ScrolledListBox      | addChild()
      Vrui.SingleChildContainer | addChild()
      Vrui.SubMenu              | addChild()

(Note that some containers are also contained objects as well.)

Contained objects have a special class method SetContained(instance, isContained) that takes a contained object instance and a flag indicating whether the object should be put in a contained state, the contained state being the non-normal Python state where the C++ container is expected to delete the C++ object underlying the Python wrapper. Contained objects in this state must be handled with care: if a Python reference still exists when a container deletes the C++ object, you will get memory corruption and probably crash the Python interpreter. If you can't guarantee that the lifetime of the container will exceed the lifetime of a Python reference to one of its contained objects, lose that reference and get the object out of the container as needed. Don't worry, the Python wrapper will stay intact, and when you access that object from of the C++ container you will get the same wrapper for the instance.

The container methods that take ownership of contained objects are specially instrumented to call SetContained(obj, True) whenever a contained object obj is passed to them.

(Note: it is also possible to specify containers that will release ownership of contained objects and therefore the ownership direction should be reverted to the normal Python state. However, Vrui has no such containers at this point.)

Two convenience functions exist to help explicitly manage the ownership direction:

      Contained(obj):   calls SetContained(obj, True) and then returns obj
      Uncontained(obj): calls SetContained(obj, False) and then returns obj

These can be used inline in expressions to streamline your code.

In the latest release, contained objects now also have a method setContained() that is called as obj.setContained(isContained).

However, it is now seldom necessary to explicitly manage the direction of ownership. Vrui has a special case of ownership-taking for its GLMotif widget containers in that GLMotif objects are constructed with their parent object and that parent object then owns the newly constructed object. Furthermore, GLMotif widget trees never change structure. Therefore, each GLMotif object is automatically “SetContained” when it is created.

There is an exception to every rule, and this rule is no exception. Vrui.PopupMenu and Vrui.PopupWindow are SetContained during construction even though their container (Vrui.WidgetManager) does not delete them. The reason is that you will often create these and then let Vrui.WidgetManager take over. If you let the reference to the Python object die without first being SetContained, then the underlying C++ object will be deleted causing a lot of trouble, gnashing of teeth, etc. So the way to handle these is to leave them SetContained and then, if you ever call popdownWidget on one, use SetContained(popup, False) or popup.setContained(False) or Uncontained(popup) to revert their ownership characteristics back to normal. When the last reference to the popup disappears, the whole object will then be deleted.

Vrui.{AL,GL}Object.DataItem objects can be contained by Vrui.{AL,GL}ContextData containers, but this only happens when a data item is passed to Vrui.{AL,GL}ContextData.addDataItem(). The Vrui.{AL,GL}ContextData object thereafter maintains ownership of the data item. However, it is not necessary to explicitly set the ownership direction in this case either; Vrui.{AL,GL}ContextData.addDataItem() does it for you.

In short, you now seldom need to manage the ownership direction of objects explicitly, with the minor exceptions being when you want to delete a Vrui.PopupMenu or a Vrui.PopupWindow.

The ReceivingCallback Paradigm

Vrui utilizes callbacks in many situations. Examples include button press and tool creation notification. These callbacks are all built upon an infrastructure involving the C++ classes Misc::CallbackList and Misc::CallbackData. Programmers can register callback functions with a callback list and these functions will later be “called back” with a callback data object describing the event.

Each particular C++ callback data class extends Misc::CallbackData with information specific to its callback. For example, GLMotif::Button's SelectCallbackData object contains a pointer to the button that was selected.

Because the dispatching of callbacks is handled generically in C++ by Misc::CallbackList, we would normally receive a generic Vrui.CallbackData in Python containing no useful information for our callbacks. At first glance, it seems that Python's type system would handle this just fine; an object is and object is an object. The difficulty is that these callback data objects have originated in Vrui and have had an entirely C++ existence until they are sent to our Python callback function. The first time that such an object “surfaces” to Python, it is given a Python wrapper and that wrapper will be constructed based only on its C++ static type at that point. This is the wrapper that the object then keeps throughout its lifetime. Therefore, we need to orchestrate the receipt of the callback data object so that when it surfaces, it's C++ static type is correct for our purposes.

In order to handle this situation, the callback data objects in Vrui:

      Vrui.ALContextData.CurrentContextDataChangedCallbackData
      Vrui.GLContextData.CurrentContextDataChangedCallbackData
      Vrui.Button.CallbackData
      Vrui.Button.ArmCallbackData
      Vrui.Button.SelectCallbackData
      Vrui.DragWidget.CallbackData
      Vrui.DragWidget.DraggingCallbackData
      Vrui.DropdownBox.CallbackData
      Vrui.DropdownBox.ValueChangedCallbackData
      Vrui.FileSelectionDialog.CallbackData
      Vrui.FileSelectionDialog.OKCallbackData
      Vrui.FileSelectionDialog.CancelCallbackData
      Vrui.ListBox.CallbackData
      Vrui.ListBox.ValueChangedCallbackData
      Vrui.ListBox.ItemSelectedCallbackData
      Vrui.ListBox.ListChangedCallbackData
      Vrui.ListBox.PageChangedCallbackData
      Vrui.Menu.CallbackData
      Vrui.Menu.EntrySelectCallbackData
      Vrui.RadioBox.CallbackData
      Vrui.RadioBox.ValueChangedCallbackData
      Vrui.ScrollBar.ValueChangedCallbackData
      Vrui.Slider.ValueChangedCallbackData
      Vrui.SubMenu.CallbackData
      Vrui.SubMenu.EntrySelectCallbackData
      Vrui.ToggleButton.ValueChangedCallbackData
      Vrui.CallbackData
      Vrui.TimerEventScheduler.CallbackData
      Vrui.CoordinateManager.CallbackData
      Vrui.CoordinateManager.CoordinateTransformChangedCallbackData
      Vrui.InputDevice.CallbackData
      Vrui.InputDevice.ButtonCallbackData
      Vrui.InputDevice.ValuatorCallbackData
      Vrui.ToolManager.ToolCreationCallbackData
      Vrui.ToolManager.ToolDestructionCallbackData

all define a wrapper method ReceivingCallback() that can wrap another function (our callback function), and the ReceivingCallback() wrapper is what you give to the callback list to be later called back upon. This way, the C++ callback data object first surfaces in its specific ReceivingCallback wrapper which, internally, casts the callback data object to the ReceivingCallback's associated C++ type, and then the object surfaces with a Python wrapper corresponding to that associated type. From there, your actual, ReceivingCallback-wrapped callback function is passed the Python-wrapped callback data object, with all of its information now accessible to Python.

For example, in:

      def resetNavigationCallback(self, cbData):
          # ... use cbData to respond to the event ...
      def setupCallbacks(self):
          button.getSelectCallbacks().add(
              Vrui.Button.SelectCallbackData.ReceivingCallback(self.resetNavigationCallback))

Vrui.Button.SelectCallbackData.ReceivingCallback() wraps resetNavigationCallback() such that cbData is downcast from a generic Vrui.CallbackData to a Vrui.Button.SelectCallbackData which contains the proper information.

You must be careful to only wrap callback functions in a way consistent with the callback data that will be sent to them, otherwise a runtime error will occur.

The *.ReceivingCallback wrappers can be used as decorators as well, as in:

      @Vrui.Button.SelectCallbackData.ReceivingCallback
      def resetNavigationCallback(self, cbData):
          # ... use cbData to respond to the event ...
      def setupCallbacks(self):
          button.getSelectCallbacks().add(self.resetNavigationCallback)

This is a nice clean approach. However, the demo program in examples/demo.py does not use this technique because resetNavigationCallback() is both used as a callback function and invoked directly in a different part of the program.

It is worthwhile considering a similar situation where such a mechanism is not required. When a Python object is created which has a C++ base, the Python wrapper will be created based on its full C++ type. Therefore, even if the object is stored generically in a C++ structure and later retrieved and passes back into Python, it will still have a full wrapper that knows all about its full type.

In summary, the important factor in how a Python-wrapped C++ object is known to Python is what information is known about it when it is first wrapped for Python. If the object is wrapped during creation on the Python side, it is fully known and no information is lost. However, if an object is created on the C++ side and is later surfaced (and wrapped), it will only have the information that was available statically at the point that it was wrapped.

Consider the situation of accessing a list of C++ objects that were created by Vrui on the C++ side. If we bring these into Python using an accessor that knows them only with a generic type, then they will thereafter only have that generic type information available on the Python side. For this reason, we probably still need a ReceivingCallback-like mechanism for things like Vislets and Tools (to name just two).

Porting Notes For Previous Versions

Vrui/SceneGraph support has been added. This is ENTIRELY UNTESTED. Some quick notes:

  • GLRenderState.contextData could not be exposed due to some compiler glitch. If necessary, this can be added in “by hand”. Let me know….
  • Probably the only the *NodePointer types should be manipulated by Python. These are shared pointer types and should play well with Python.

– Application definition and launching are now much simpler using Vrui.App and Vrui.AppStringArray. See the section "Simplified Interface to Vrui Application Objects" above.

– Two demos exist in the examples subdirectory: demo.py and clusterDemo.py. clusterDemo.py has now been commented to explain the use of the ActionQueue class it utilizes to distribute work in CAVE-like clusters.

– You can safely remove Vrui.Contained() from around all GLMotif widgets. This will now happen automatically.

– Be sure to call superclass init methods to ensure object identity as in the following:

      class DemoApp(Vrui.App, Vrui.GLObject):
          def __init__(self, *args, **kwargs):
              Vrui.App.__init__(self, *args, **kwargs)
              Vrui.GLObject.__init__(self)
              # ... other initialization for your application ...

– You may now replace constructions like:

          def display(self, context):
              di = Vrui.RetrieveDataItem(context, self)

with:

          def display(self, context):
              di = context.retrieveDataItem(self)

– You must now replace constructions like:

      Vrui.CallbackList.add(
          button.getSelectCallbacks(),
          Vrui.Button.SelectCallbackData.ReceivingCallback(self.resetNavigationCallback) )

with:

      Vrui.CallbackList.Add(
          button.getSelectCallbacks(),
          Vrui.Button.SelectCallbackData.ReceivingCallback(self.resetNavigationCallback) )

or better yet:

      button.getSelectCallbacks().add(
          Vrui.Button.SelectCallbackData.ReceivingCallback(self.resetNavigationCallback) )

Similarly for Vrui.CallbackList.AddToFront() and Vrui.CallbackList.Remove()

– The number of stub files generated has been increased from 9 to 16 because things got a lot bigger with the addition of the SceneGraph stuff. I was noticing that the CAVE head node was slowing down a lot compiling one of the generated C++ stub files which was about 0.75 MB in size!

-END-

keckcaves/pyvrui_release_notes_-_10-july-2009.txt · Last modified: 2009/07/10 10:29 by epuckett