Miscellaneous tutorial information

This tutorial began with excerpts from emails that I've written to help people get up and running with the groovx software; its content may overlap somewhat with the information in the toplevel README.

Table of contents:


1. Getting the source

You can find a daily tarball of groovx here:

or here (substitute today's date for YYYYMMDD):

Optionally, if you're interested in playing around with the visual object editor program, you can also build itcl (incr tcl) and iwidgets (incr widgets), for which I also have tarballs:


2. Building and installing the software and its dependencies

Here's a quick rundown on how I would configure+install all this stuff.

Assuming you have all the tarballs sitting in a "$BUILD" directory somewhere (e.g. perhaps ~/build/), and that you'd like to install everything to $INSTALL (e.g. perhaps ~/local or /usr/local), then

     ### unpack all the tarballs

     cd $BUILD
     tar xfz tcl8.5a1-src.tar.gz
     tar xfz tk8.5a1-src.tar.gz
     tar xfz itcl3.2.1_src.tgz         # optional
     tar xfz iwidgets4.0.1.tar.gz      # optional
     tar xfz groovx_20041004.tar.gz

     ### configure+build tcl

     cd $BUILD/tcl8.5a1/unix
     ./configure --prefix=$INSTALL/tcl8.5a1
     make
     make install

     ### configure+build tk

     cd $BUILD/tk8.5a1/unix
     ./configure --prefix=$INSTALL/tcl8.5a1
     make
     make install

     ### optional: configure+build itcl

     cd $BUILD/itcl3.2.1
     ./configure  --prefix=$INSTALL/tcl8.5a1
     make
     make install

     ### optional: configure+build iwidgets (this requires itcl)

     cd $BUILD/iwidgets4.0.1
     ./configure  --prefix=$INSTALL/tcl8.5a1 \
       --with-itcl=$BUILD/itcl3.2.1 \
       --with-itk=$BUILD/itcl3.2.1
     # no need to do "make", just do "make install"
     make install

     ### now configure+build groovx itself

     cd $BUILD/groovx_20041004
     ./configure \
       --prefix=$INSTALL/groovx \
       --with-tcltk=$INSTALL/tcl8.5a1 \
       --disable-matlab
     # this will make+install everything, plus run a test suite:
     make

Note that any of the configure scripts will recognize a --help option (i.e. do "./configure --help"), causing the script to print a list of all the configure-time options that it accepts.


3. Using the interactive shell 'groovx'

The main executable program that gets built is a shell called 'groovx'. This is essentially a custom Tcl+Tk shell, that includes a large number of new commands that implement the graphical-object and visual-experiment functionality of groovx.

You can start the shell interactively by calling $INSTALL/groovx/bin/groovx (where $INSTALL is the install directory you chose when configuring groovx). Of course, if $INSTALL/groovx/bin is in your $PATH environment variable, then you can just call 'groovx'.

Here's the obligatory "hello, world" example for groovx. Assuming you start from a standard shell prompt ("$"), you start the groovx shell, and then issue a series of commands. The groovx prompt ends with ">>>", so everything following that prompt on a given line is what you would type. Here, I've added comments following "#" characters which you would not see or type during an actual shell session.

     [sideswipe 13:26 53]$ groovx

     GroovX 1.0a1 (Feb 22 2005)
     Copyright (c) 1998-2004 California Institute of Technology
     Copyright (c) 2004-2007 University of Southern California
     GroovX is free software, covered by the GNU General Public License, and you are
     welcome to change it and/or distribute copies of it under certain conditions.
             startup time (tcl+tk)  0.160s (user)  0.019s (sys)  0.198s (wall)
             startup time (GroovX)  0.151s (user)  0.062s (sys)  0.395s (wall)

     groovx>>> set g [new GxText]
     19                               # 19 is the id of the new text object

     groovx>>> -> $g text "Hello, World"

     groovx>>> see $g

Now you should see the text "Hello, World" in your groovx window.

As an aside, one way to get a list of all the classnames known to the "new" command is to call the "new" command with a bogus classname (such as the ever-popular "foobar"):

     groovx>>> new foobar
     Obj::new: known types are:
             AbortTrialEvent
             AllowResponsesEvent
             Bitmap
             Block
             ClearBufferEvent
             CloneFace
             DenyResponsesEvent
             DrawEvent
             EndTrialEvent
             EventResponseHdlr
             ExptDriver
             Face
             FileWriteEvent
             FilledFace
             FinishDrawingEvent
             Fish
             FixPt
             Gabor
             GaborArray
             GenericEvent
             Gtext
             GxColor
             GxCylinder
             GxDisk
             GxDrawStyle
             GxEmptyNode
             GxFixedScaleCamera
             GxLighting
             GxLine
             GxMaterial
             GxPerspectiveCamera
             GxPixmap
             GxPointSet
             GxPsyphyCamera
             GxScaler
             GxSeparator
             GxSphere
             GxText
             GxTransform
             House
             InnerFace
             Jitter
             KbdResponseHdlr
             MaskHatch
             MorphyFace
             MultiEvent
             NextNodeEvent
             NullResponseHdlr
             NullTrialEvent
             output_file
             RenderBackEvent
             RenderEvent
             RenderFrontEvent
             Sound
             SwapBuffersEvent
             TimingHandler
             TimingHdlr
             Toglet
             TopToglet
             Trial
             UndrawEvent
     unknown object type 'foobar' (rutz::error)

Let's return to our "Hello, World" example and explore a little deeper. Groovx is a roughly object-oriented system (after all the acronym was originally conceived as a GRaphical Object-Oriented Visual eXperiments). So, in our example, what we have stored in the variable "g" is the unique object-id (or UID) for a GxText object. UIDs in groovx are like raw addresses in C or C++; they uniquely refer to a particular object. Groovx uses a reference-counting system behind the scenes.

The arrow syntax with the pseudo-operator "->" is just a bit of syntactic sugar. Note that the order of arguments is different than in C or C++ -- there, you might say

     ptr->someFunction()

but in groovx you'd say

     -> $ptr someFunction"

In fact, with Tcl's extremely simple and uniform syntax, "->" is just a command like any other, only with a strange-looking name. So, the following two commands are precisely equivalent:

     -> $object command arg1 arg2 arg3
     Class::command $object arg1 arg2 arg3

where "Class" is the class of the given $object. What "->" does is to look up the class of $object, and then call the command as shown in the second form above.

OK, we've seen what "->" means, now let's use it to explore the text object above.

First, let's see what its type is:

     groovx>>> -> $g type
     GxText

Good, that makes sense since we created it by calling "new GxText".

Given that its type is GxText, we know that calling "-> $g command" is equivalent to calling "GxText::command $g". Now, when Tcl is running in interactive mode, if you type a partial command name, Tcl will try to make a complete command name out of the partial name. If there is a unique match, Tcl will call it; otherwise, it will helpfully list all possible matches. So in our case, we do the following to get a list of all possible commands in the GxText:: namespace:

     groovx>>> GxText::
     ambiguous command name "GxText::": ::GxText::alignmentMode
     ::GxText::allFields ::GxText::aspectRatio ::GxText::bbVisibility
     ::GxText::boundingBox ::GxText::category ::GxText::centerX
     ::GxText::centerY ::GxText::contains ::GxText::countAll
     ::GxText::decr_ref_count ::GxText::deepChildren ::GxText::delete
     ::GxText::fields ::GxText::findAll ::GxText::font
     ::GxText::height ::GxText::heightFactor ::GxText::incr_ref_count
     ::GxText::is ::GxText::loadASW ::GxText::loadGVX
     ::GxText::maxDimension ::GxText::new ::GxText::newarr
     ::GxText::readASW ::GxText::readLGX ::GxText::realType
     ::GxText::refCount ::GxText::removeAll ::GxText::renderMode
     ::GxText::retrieveASW ::GxText::saveASW ::GxText::saveGVX
     ::GxText::savePS ::GxText::scalingMode ::GxText::strokeWidth
     ::GxText::text ::GxText::traceOff ::GxText::traceOn
     ::GxText::traceStatus ::GxText::traceToggle ::GxText::type
     ::GxText::width ::GxText::widthFactor ::GxText::writeASW
     ::GxText::writeGVX ::GxText::writeLGX ::GxText::xml_debug

So this shows us all possible commands that we can call on a GxText object. You'll notice that among them is GxText::type, the command that we previously used to get the type of the object.

Groovx has a mostly-working self-documentation system. For all groovx commands, you can use "?" to get information about how to use the command. For example, for GxText::text, you can do the following:

     groovx>>> ? GxText::text
     GxText::text resolves to ::GxText::text
             ::GxText::text objref(s)
             ::GxText::text objref(s) new_val(s)

What that result tells you is that there are two syntaxes for calling the GxText::text command: in the first, you just call it with an objref (or object-id), in which case it returns the current 'text' value for that object; in the second, you call it with an objref plus a new 'text' value (that's how we specified the "Hello, World" value previously).

One thing to note is that with the way that "inheritance" currently works in groovx (by using Tcl's namespace importing mechanism), inherited commands will appear under their base name when looked up with "?". Thus:

     groovx>>> ? GxText::type
     GxText::type resolves to ::Obj::type
             ::Obj::type objref(s)
             (defined at src/io/objtcl.cc:205)

shows that GxType::type is an inherited command, with the original command being Obj::type.

Similarly with GxText::loadGVX, which is inherited from IO::loadGVX:

     groovx>>> ? GxText::loadGVX
     GxText::loadGVX resolves to ::IO::loadGVX
             ::IO::loadGVX filename
             (defined at src/io/iotcl.cc:69)

Finally, you can even use the "?" command to look up itself:

     groovx 1>>> ? ?
     ? resolves to ::?
             ::? cmd_name
             (defined at src/tcl/misctcl.cc:148)

A useful tool for inspecting objects involves using the persistence layer built into the C++ framework. This is a generic serialization API that allows objects to serialize and deserialize independent of the "backend" -- that is, independent of the ultimate storage medium and formatting.

One of the available formats is an XML format (called the GVX format, using filenames with a .gvx extension). There are commands to read/write GVX format from strings in the groovx shell, as well as to read/write GVX format to files in the filesystem.

To dump an object's contents to a string in the groovx shell, do the following (assuming you still have the $g object with "Hello, World" still sitting around):

     groovx>>> -> $g writeGVX
     <?xml version="1.0"?>
     <!-- GroovX XML 1 -->
     <object type="GxText" id="1" name="root" version="3">
             <string name="text">Hello, World</string>
             <int name="strokeWidth" value="2"/>
             <baseclass type="GxShapeKit" id="2" name="GxShapeKit" version="3">
                     <int name="category" value="-1"/>
                     <int name="renderMode" value="1"/>
                     <bool name="bbVisibility" value="0"/>
                     <int name="scalingMode" value="2"/>
                     <double name="widthFactor" value="2.46548"/>
                     <double name="heightFactor" value="2.46548"/>
                     <int name="alignmentMode" value="2"/>
                     <double name="centerX" value="0"/>
                     <double name="centerY" value="0"/>
             </baseclass>
     </object>

This is a useful way to garner some insight into the internals of the toolkit. You'll see that GxText has "GxShapeKit" as a subclass. You'll also see that the text message "Hello, World" is displayed as well. To capture these GVX contents in a string, do the following (reminder: square brackets [] in Tcl are used to evaluate the expression inside, and return its result):

     groovx>>> set s [-> $g writeGVX]

Similarly, GVX output can be written to a file using saveGVX, like this:

     groovx>>> -> $g saveGVX foo.gvx

You can verify the contents of the file "foo.gvx" by doing "cat foo.gvx" at the groovx shell prompt (in interactive mode, if Tcl sees a command it doesn't recognize it will first look to see if that command is an executable in the user's $PATH, which should be the case for "cat").

Now, to restore the contents of the foo.gvx file at a later time, use the IO::loadGVX command:

     groovx>>> set g2 [IO::loadGVX foo.gvx]
     40

You can verify that $g2 now holds the same values as $g by doing "-> $g2 writeGVX".

Also note that groovx can transparently handle compressed files in most cases. For saving files, groovx will apply gzip compression if you give it a filename ending in .gz, like this:

     groovx>>> -> $g saveGVX foo.gvx.gz

Verify that compression actually took place using Tcl's "file size" command to get the size of each file in bytes:

     groovx>>> file size foo.gvx
     630
     groovx>>> file size foo.gvx.gz
     301

For loading files, compression is handled completely transparently:

     groovx>>> set g3 [IO::loadGVX foo.gvx]
     groovx>>> set g4 [IO::loadGVX foo.gvx.gz]


4. Using the groovx shell from a script

You can use the groovx shell in the "shbang" line of a shell-script, i.e. the first line of a script starting with "#!". Again, if $INSTALL/groovx/bin is in your $PATH, then you can use

#!/usr/bin/env groovx

for the shbang line. Alternatively, you could use that same shbang form if you have created a symbolic link in some directory that IS in your $PATH, pointing to groovx's real location, e.g.:

ln -s $INSTALL/groovx/bin /usr/local/bin/groovx

Otherwise, you can specify the full path to the groovx executable in the shbang line:

#!/path/to/installation/groovx/bin/groovx


5. Running a sample visual experiment 'sample_expt.tcl'

Once you've built+installed groovx, you can run some of the sample scripts that will now be located in $INSTALL/groovx/bin ... in particular, you can run this script

     $INSTALL/groovx/bin/sample_expt.tcl

to get a sample version of my free-viewing psychophysics experiment. The original source for that script would be found in $BUILD/groovx/share/scripts/sample_expt.tcl if you want to read the source, experiment with changing it, etc.

When you run sample_expt.tcl, it will go through 10 free-viewing trials with some sample images I've included with groovx (you have to actually respond on the response screens by pressing '1' or '2' to keep the experiment moving along, thought). Once the experiment quits, you'll be left with three files like the following:

The .gvx file is an XML file that contains the entire object tree of your experiment, and should begin something like the following:

     <?xml version="1.0"?>
     <!-- GroovX XML 1 -->
     <object type="ExptDriver" id="1" name="root" version="6">
             <string name="hostname">localhost.localdomain</string>
             <string name="subject">/home/rjpeters/tmp/groovx_20041004</string>
             <string name="beginDate">Mon Oct 04 19:27:10 PDT 2004</string>
             <string name="endDate">Mon Oct 04 19:28:11 PDT 2004</string>
             <string name="autosaveFile">__autosave_file</string>
             <int name="autosavePeriod" value="10"/>
             <string name="infoLog">@Mon Oct 04 19:27:09 PDT 2004 groovx /home/rjpeters/local/groovx_20041004/bin/sample_expt.tcl
     @Mon Oct 04 19:27:10 PDT 2004 Beginning experiment.
     @Mon Oct 04 19:27:46 PDT 2004 Experiment saved.
     @Mon Oct 04 19:28:11 PDT 2004 Experiment complete.
     </string>
             <ownedobj type="tcl::ProcWrapper" id="2" name="doWhenComplete" version="0">
                     <string name="args"/>
                     <string name="body">exit</string>
             </ownedobj>

     ...

The .log file contains a verbose log of everything that happened during the experiment, and may be useful for debugging as well as for checking how the exact timing of things played out during the experiment. Each line of the log file shows the current time (in milliseconds) relative to various reference points; e.g. the following line

     ExptDriver(19) @ 3549.279 | Trial(286) @ 44.388 | RenderBackEvent(462) @ 0.362 | entering RenderBackEvent(462)

was written at 3549ms after the start of the experiment (which was object number 19), and at 44ms after the start of Trial (object number 286), and 0.36ms after the start of the RenderBackEvent.

Lines that may be of particular interest:

       ExptDriver(19) @ 8057.190 | Trial(286) @ 4552.299 | event_info: 1

       ExptDriver(19) @ 8057.439 | Trial(286) @ 4552.548 | response val: 1

Here's what an excerpt of the log file contents showing a single trial.

     ExptDriver(19) @ 3517.003 | EndTrialEvent(599) @ 13.400 | Trial(286) @ 12.112 | entering Trial(286)
     ExptDriver(19) @ 3517.237 | EndTrialEvent(599) @ 13.634 | Trial(286) @ 12.346 | image /home/rjpeters/local/groovx_20041004/bin/../share/images/nga02.jpg
     ExptDriver(19) @ 3517.398 | EndTrialEvent(599) @ 13.795 | Trial(286) @ 12.507 | binding to ::__ERHPrivate::handler1
     ExptDriver(19) @ 3517.709 | EndTrialEvent(599) @ 14.106 | Trial(286) @ 12.818 | scheduled @ 1: DenyResponsesEvent(458)
     ExptDriver(19) @ 3517.876 | EndTrialEvent(599) @ 14.273 | Trial(286) @ 12.985 | scheduled @ 25: RenderBackEvent(462)
     ExptDriver(19) @ 3518.013 | EndTrialEvent(599) @ 14.410 | Trial(286) @ 13.122 | scheduled @ 50: RenderEvent(466)
     ExptDriver(19) @ 3518.168 | EndTrialEvent(599) @ 14.565 | Trial(286) @ 13.277 | scheduled @ 100: MultiEvent(470)
     ExptDriver(19) @ 3518.307 | EndTrialEvent(599) @ 14.704 | Trial(286) @ 13.416 | scheduled @ 300: NextNodeEvent(482)
     ExptDriver(19) @ 3518.448 | EndTrialEvent(599) @ 14.845 | Trial(286) @ 13.557 | scheduled @ 500: ClearBufferEvent(486)
     ExptDriver(19) @ 3518.586 | EndTrialEvent(599) @ 14.983 | Trial(286) @ 13.695 | scheduled @ 600: RenderEvent(490)
     ExptDriver(19) @ 3518.730 | EndTrialEvent(599) @ 15.127 | Trial(286) @ 13.839 | scheduled @ 1000: MultiEvent(494)
     ExptDriver(19) @ 3518.868 | EndTrialEvent(599) @ 15.265 | Trial(286) @ 13.977 | scheduled @ 1100: NextNodeEvent(506)
     ExptDriver(19) @ 3519.009 | EndTrialEvent(599) @ 15.406 | Trial(286) @ 14.118 | scheduled @ 1200: ClearBufferEvent(510)
     ExptDriver(19) @ 3519.147 | EndTrialEvent(599) @ 15.544 | Trial(286) @ 14.256 | scheduled @ 1300: RenderEvent(514)
     ExptDriver(19) @ 3519.291 | EndTrialEvent(599) @ 15.688 | Trial(286) @ 14.400 | scheduled @ 4000: MultiEvent(518)
     ExptDriver(19) @ 3519.426 | EndTrialEvent(599) @ 15.823 | Trial(286) @ 14.535 | leaving EndTrialEvent(599)
     ExptDriver(19) @ 3519.596 | Trial(286) @ 14.705 | DenyResponsesEvent(458) @ 0.095 | entering DenyResponsesEvent(458)
     ExptDriver(19) @ 3519.716 | Trial(286) @ 14.825 | DenyResponsesEvent(458) @ 0.215 | req 1 - -0
     ExptDriver(19) @ 3519.835 | Trial(286) @ 14.944 | DenyResponsesEvent(458) @ 0.334 | trDenyResponses
     ExptDriver(19) @ 3520.016 | Trial(286) @ 15.125 | DenyResponsesEvent(458) @ 0.515 | leaving DenyResponsesEvent(458)
     ExptDriver(19) @ 3549.279 | Trial(286) @ 44.388 | RenderBackEvent(462) @ 0.362 | entering RenderBackEvent(462)
     ExptDriver(19) @ 3549.480 | Trial(286) @ 44.589 | RenderBackEvent(462) @ 0.563 | req 25 - -0
     ExptDriver(19) @ 3549.681 | Trial(286) @ 44.790 | RenderBackEvent(462) @ 0.764 | leaving RenderBackEvent(462)
     ExptDriver(19) @ 3574.454 | Trial(286) @ 69.563 | RenderEvent(466) @ 0.161 | entering RenderEvent(466)
     ExptDriver(19) @ 3574.588 | Trial(286) @ 69.697 | RenderEvent(466) @ 0.295 | req 50 - -0
     ExptDriver(19) @ 3574.757 | Trial(286) @ 69.866 | RenderEvent(466) @ 0.464 | current node is GxSeparator(241)
     ExptDriver(19) @ 3576.642 | Trial(286) @ 71.751 | RenderEvent(466) @ 2.349 | leaving RenderEvent(466)
     ExptDriver(19) @ 3623.771 | Trial(286) @ 118.880 | MultiEvent(470) @ 0.160 | entering MultiEvent(470)
     ExptDriver(19) @ 3623.906 | Trial(286) @ 119.015 | MultiEvent(470) @ 0.295 | req 100 - -0
     ExptDriver(19) @ 3624.069 | Trial(286) @ 119.178 | MultiEvent(470) @ 0.458 | SwapBuffersEvent(474) @ 0.111 | entering SwapBuffersEvent(474)
     ExptDriver(19) @ 3624.214 | Trial(286) @ 119.323 | MultiEvent(470) @ 0.603 | SwapBuffersEvent(474) @ 0.256 | req 0 - -0
     ExptDriver(19) @ 3624.410 | Trial(286) @ 119.519 | MultiEvent(470) @ 0.799 | SwapBuffersEvent(474) @ 0.452 | leaving SwapBuffersEvent(474)
     ExptDriver(19) @ 3624.585 | Trial(286) @ 119.694 | MultiEvent(470) @ 0.974 | FinishDrawingEvent(478) @ 0.111 | entering FinishDrawingEvent(478)
     ExptDriver(19) @ 3624.731 | Trial(286) @ 119.840 | MultiEvent(470) @ 1.120 | FinishDrawingEvent(478) @ 0.257 | req 0 - -0
     ExptDriver(19) @ 3637.045 | Trial(286) @ 132.154 | MultiEvent(470) @ 13.434 | FinishDrawingEvent(478) @ 12.571 | leaving FinishDrawingEvent(478)
     ExptDriver(19) @ 3637.210 | Trial(286) @ 132.319 | MultiEvent(470) @ 13.599 | leaving MultiEvent(470)
     ExptDriver(19) @ 3823.634 | Trial(286) @ 318.743 | NextNodeEvent(482) @ 0.165 | entering NextNodeEvent(482)
     ExptDriver(19) @ 3823.769 | Trial(286) @ 318.878 | NextNodeEvent(482) @ 0.300 | req 300 - -0
     ExptDriver(19) @ 3823.904 | Trial(286) @ 319.013 | NextNodeEvent(482) @ 0.435 | leaving NextNodeEvent(482)
     ExptDriver(19) @ 4023.610 | Trial(286) @ 518.719 | ClearBufferEvent(486) @ 0.168 | entering ClearBufferEvent(486)
     ExptDriver(19) @ 4023.774 | Trial(286) @ 518.883 | ClearBufferEvent(486) @ 0.332 | req 500 - -0
     ExptDriver(19) @ 4023.986 | Trial(286) @ 519.095 | ClearBufferEvent(486) @ 0.544 | leaving ClearBufferEvent(486)
     ExptDriver(19) @ 4123.581 | Trial(286) @ 618.690 | RenderEvent(490) @ 0.162 | entering RenderEvent(490)
     ExptDriver(19) @ 4123.715 | Trial(286) @ 618.824 | RenderEvent(490) @ 0.296 | req 600 - -0
     ExptDriver(19) @ 4123.881 | Trial(286) @ 618.990 | RenderEvent(490) @ 0.462 | current node is GxPixmap(266)
     ExptDriver(19) @ 4135.669 | Trial(286) @ 630.778 | RenderEvent(490) @ 12.250 | loaded image file /home/rjpeters/local/groovx_20041004/bin/../share/images/nga02.jpg
     ExptDriver(19) @ 4154.162 | Trial(286) @ 649.271 | RenderEvent(490) @ 30.743 | leaving RenderEvent(490)
     ExptDriver(19) @ 4523.635 | Trial(286) @ 1018.744 | MultiEvent(494) @ 0.162 | entering MultiEvent(494)
     ExptDriver(19) @ 4523.771 | Trial(286) @ 1018.880 | MultiEvent(494) @ 0.298 | req 1000 - -0
     ExptDriver(19) @ 4523.935 | Trial(286) @ 1019.044 | MultiEvent(494) @ 0.462 | SwapBuffersEvent(498) @ 0.111 | entering SwapBuffersEvent(498)
     ExptDriver(19) @ 4524.081 | Trial(286) @ 1019.190 | MultiEvent(494) @ 0.608 | SwapBuffersEvent(498) @ 0.257 | req 0 - -0
     ExptDriver(19) @ 4524.280 | Trial(286) @ 1019.389 | MultiEvent(494) @ 0.807 | SwapBuffersEvent(498) @ 0.456 | leaving SwapBuffersEvent(498)
     ExptDriver(19) @ 4524.456 | Trial(286) @ 1019.565 | MultiEvent(494) @ 0.983 | FinishDrawingEvent(502) @ 0.112 | entering FinishDrawingEvent(502)
     ExptDriver(19) @ 4524.602 | Trial(286) @ 1019.711 | MultiEvent(494) @ 1.129 | FinishDrawingEvent(502) @ 0.258 | req 0 - -0
     ExptDriver(19) @ 4536.783 | Trial(286) @ 1031.892 | MultiEvent(494) @ 13.310 | FinishDrawingEvent(502) @ 12.439 | leaving FinishDrawingEvent(502)
     ExptDriver(19) @ 4536.938 | Trial(286) @ 1032.047 | MultiEvent(494) @ 13.465 | leaving MultiEvent(494)
     ExptDriver(19) @ 4623.734 | Trial(286) @ 1118.843 | NextNodeEvent(506) @ 0.162 | entering NextNodeEvent(506)
     ExptDriver(19) @ 4623.870 | Trial(286) @ 1118.979 | NextNodeEvent(506) @ 0.298 | req 1100 - -0
     ExptDriver(19) @ 4624.003 | Trial(286) @ 1119.112 | NextNodeEvent(506) @ 0.431 | leaving NextNodeEvent(506)
     ExptDriver(19) @ 4721.174 | Trial(286) @ 1216.283 | ClearBufferEvent(510) @ 0.167 | entering ClearBufferEvent(510)
     ExptDriver(19) @ 4721.312 | Trial(286) @ 1216.421 | ClearBufferEvent(510) @ 0.305 | req 1200 - -0
     ExptDriver(19) @ 4721.526 | Trial(286) @ 1216.635 | ClearBufferEvent(510) @ 0.519 | leaving ClearBufferEvent(510)
     ExptDriver(19) @ 4823.696 | Trial(286) @ 1318.805 | RenderEvent(514) @ 0.158 | entering RenderEvent(514)
     ExptDriver(19) @ 4823.832 | Trial(286) @ 1318.941 | RenderEvent(514) @ 0.294 | req 1300 - -0
     ExptDriver(19) @ 4824.001 | Trial(286) @ 1319.110 | RenderEvent(514) @ 0.463 | current node is GxSeparator(101)
     ExptDriver(19) @ 4828.685 | Trial(286) @ 1323.794 | RenderEvent(514) @ 5.147 | leaving RenderEvent(514)
     ExptDriver(19) @ 7523.559 | Trial(286) @ 4018.668 | MultiEvent(518) @ 0.300 | entering MultiEvent(518)
     ExptDriver(19) @ 7523.743 | Trial(286) @ 4018.852 | MultiEvent(518) @ 0.484 | req 4000 - -0
     ExptDriver(19) @ 7523.956 | Trial(286) @ 4019.065 | MultiEvent(518) @ 0.697 | SwapBuffersEvent(522) @ 0.157 | entering SwapBuffersEvent(522)
     ExptDriver(19) @ 7524.149 | Trial(286) @ 4019.258 | MultiEvent(518) @ 0.890 | SwapBuffersEvent(522) @ 0.350 | req 0 - -0
     ExptDriver(19) @ 7524.399 | Trial(286) @ 4019.508 | MultiEvent(518) @ 1.140 | SwapBuffersEvent(522) @ 0.600 | leaving SwapBuffersEvent(522)
     ExptDriver(19) @ 7542.130 | Trial(286) @ 4037.239 | MultiEvent(518) @ 18.871 | FinishDrawingEvent(526) @ 17.662 | entering FinishDrawingEvent(526)
     ExptDriver(19) @ 7542.390 | Trial(286) @ 4037.499 | MultiEvent(518) @ 19.131 | FinishDrawingEvent(526) @ 17.922 | req 0 - -0
     ExptDriver(19) @ 7542.593 | Trial(286) @ 4037.702 | MultiEvent(518) @ 19.334 | FinishDrawingEvent(526) @ 18.125 | leaving FinishDrawingEvent(526)
     ExptDriver(19) @ 7542.787 | Trial(286) @ 4037.896 | MultiEvent(518) @ 19.528 | AllowResponsesEvent(530) @ 0.117 | entering AllowResponsesEvent(530)
     ExptDriver(19) @ 7543.574 | Trial(286) @ 4038.683 | MultiEvent(518) @ 20.315 | AllowResponsesEvent(530) @ 0.904 | req 0 - -0
     ExptDriver(19) @ 7543.778 | Trial(286) @ 4038.887 | MultiEvent(518) @ 20.519 | AllowResponsesEvent(530) @ 1.108 | trAllowResponses
     ExptDriver(19) @ 7543.983 | Trial(286) @ 4039.092 | MultiEvent(518) @ 20.724 | AllowResponsesEvent(530) @ 1.313 | binding to ::__ERHPrivate::handler1
     ExptDriver(19) @ 7544.337 | Trial(286) @ 4039.446 | MultiEvent(518) @ 21.078 | AllowResponsesEvent(530) @ 1.667 | leaving AllowResponsesEvent(530)
     ExptDriver(19) @ 7546.764 | Trial(286) @ 4041.873 | MultiEvent(518) @ 23.505 | leaving MultiEvent(518)
     ExptDriver(19) @ 8057.190 | Trial(286) @ 4552.299 | event_info: 1
     ExptDriver(19) @ 8057.439 | Trial(286) @ 4552.548 | response val: 1
     ExptDriver(19) @ 8057.578 | Trial(286) @ 4552.687 | trProcessResponse
     ExptDriver(19) @ 8057.781 | Trial(286) @ 4552.890 | MultiEvent(534) @ 0.090 | entering MultiEvent(534)
     ExptDriver(19) @ 8057.900 | Trial(286) @ 4553.009 | MultiEvent(534) @ 0.209 | req 0 - -0
     ExptDriver(19) @ 8058.060 | Trial(286) @ 4553.169 | MultiEvent(534) @ 0.369 | ClearBufferEvent(538) @ 0.111 | entering ClearBufferEvent(538)
     ExptDriver(19) @ 8058.206 | Trial(286) @ 4553.315 | MultiEvent(534) @ 0.515 | ClearBufferEvent(538) @ 0.257 | req 0 - -0
     ExptDriver(19) @ 8058.437 | Trial(286) @ 4553.546 | MultiEvent(534) @ 0.746 | ClearBufferEvent(538) @ 0.488 | leaving ClearBufferEvent(538)
     ExptDriver(19) @ 8058.610 | Trial(286) @ 4553.719 | MultiEvent(534) @ 0.919 | SwapBuffersEvent(542) @ 0.110 | entering SwapBuffersEvent(542)
     ExptDriver(19) @ 8058.756 | Trial(286) @ 4553.865 | MultiEvent(534) @ 1.065 | SwapBuffersEvent(542) @ 0.256 | req 0 - -0
     ExptDriver(19) @ 8069.660 | Trial(286) @ 4564.769 | MultiEvent(534) @ 11.969 | SwapBuffersEvent(542) @ 11.160 | leaving SwapBuffersEvent(542)
     ExptDriver(19) @ 8069.847 | Trial(286) @ 4564.956 | MultiEvent(534) @ 12.156 | FinishDrawingEvent(546) @ 0.115 | entering FinishDrawingEvent(546)
     ExptDriver(19) @ 8069.998 | Trial(286) @ 4565.107 | MultiEvent(534) @ 12.307 | FinishDrawingEvent(546) @ 0.266 | req 0 - -0
     ExptDriver(19) @ 8094.967 | Trial(286) @ 4590.076 | MultiEvent(534) @ 37.276 | FinishDrawingEvent(546) @ 25.235 | leaving FinishDrawingEvent(546)
     ExptDriver(19) @ 8095.148 | Trial(286) @ 4590.257 | MultiEvent(534) @ 37.457 | leaving MultiEvent(534)
     ExptDriver(19) @ 8095.274 | Trial(286) @ 4590.383 | scheduled @ 0: MultiEvent(534)
     ExptDriver(19) @ 8095.394 | Trial(286) @ 4590.503 | scheduled @ 500: EndTrialEvent(550)
     ExptDriver(19) @ 8603.020 | Trial(286) @ 5098.129 | EndTrialEvent(550) @ 0.163 | entering EndTrialEvent(550)
     ExptDriver(19) @ 8603.157 | Trial(286) @ 5098.266 | EndTrialEvent(550) @ 0.300 | req 500 - -0
     ExptDriver(19) @ 8603.275 | Trial(286) @ 5098.384 | EndTrialEvent(550) @ 0.418 | Trial::trEndTrial
     ExptDriver(19) @ 8603.604 | Trial(286) @ 5098.713 | EndTrialEvent(550) @ 0.747 | leaving Trial(286)
     ExptDriver(19) @ 8603.752 | EndTrialEvent(550) @ 0.895 | current element Trial(370), completed 2 of 11

Finally, the .resp file gives a summary of the responses across all trials, like this:

     %  Trial        N  Average     msec   description
          265        1     2.00  4719.00 % image /home/rjpeters/local/groovx_20041004/bin/../share/images/nga01.jpg
          286        1     1.00  4539.00 % image /home/rjpeters/local/groovx_20041004/bin/../share/images/nga02.jpg
          307        1     1.00  6726.00 % image /home/rjpeters/local/groovx_20041004/bin/../share/images/nga03.jpg
          328        1     1.00  4420.00 % image /home/rjpeters/local/groovx_20041004/bin/../share/images/nga04.jpg
          349        1     2.00  6858.00 % image /home/rjpeters/local/groovx_20041004/bin/../share/images/nga05.jpg
          370        1     2.00  4558.00 % image /home/rjpeters/local/groovx_20041004/bin/../share/images/nga06.jpg
          391        1     2.00  4468.00 % image /home/rjpeters/local/groovx_20041004/bin/../share/images/nga07.jpg
          412        1     1.00  5305.00 % image /home/rjpeters/local/groovx_20041004/bin/../share/images/nga08.jpg
          433        1     1.00  5082.00 % image /home/rjpeters/local/groovx_20041004/bin/../share/images/nga09.jpg
          454        1     1.00  5114.00 % image /home/rjpeters/local/groovx_20041004/bin/../share/images/nga10.jpg
          603        1     0.00  2496.00 % text message 'FREE-VIEWING SEQUENCE\n\nafter each image:\n  press '1' if the region labeled '1' contained the most interesting feature\n  press '2' if the region labeled '2' contained the most interesting feature\n\n[press the spacebar to continue]'

It's in a format that is suitable for importing into matlab using matlab's "load", since all of the non-numeric stuff is commented out with the % characters. The "Trial" column shows the object number of each trial; "N" shows how many times each trial was run; "Average" gives the average response value across those trials; "msec" gives the average response time of each response (in milliseconds, relative to the start of the corresponding trial); "description" just gives a textual description of the trial.


6. Exploring the C++ internals

I hope I've done a decent job of keeping the C++ code organized, but nevertheless it can be difficult to automatically see the relationships between the script (tcl/groovx) side of things and the underlying C++ code.

There are a couple mechanisms in place to help with the discovery of these relationships, involving different ways to get referred to specific places in the C++ codebase.

First of all, any time an error is generated by the groovx C++ code, a backtrace is stored internally (assuming that groovx was configured with --enable-debug, which is the default). To retrieve this backtrace, use the "bt" command from the groovx interpreter. For example, let's try to create an unknown object type:

     groovx>>> new foobar
     ...long error message...

Now, we can use "bt" to see where things went wrong:

     groovx>>> bt
     [ 0] nub::obj_mgr::new_obj(const fstring&) (src/util/objmgr.cc:47)
     [ 1] tcl/Obj::new                        (src/io/objtcl.cc:206)
     [ 2] tcl::command_group::rawInvoke        (src/tcl/tclcommandgroup.cc:285)
     [ 3] tcl::MainImpl::execCommand          (src/tcl/tclmain.cc:397)
     [ 4] tcl::MainImpl::handleLine           (src/tcl/tclmain.cc:345)
     [ 5] tcl::MainImpl::readlineLineComplete (src/tcl/tclmain.cc:298)
     [ 6] tcl::MainImpl::grabInput            (src/tcl/tclmain.cc:318)
     [ 7] tcl::MainImpl::stdinProc            (src/tcl/tclmain.cc:521)
     [ 8] tcl::MainImpl::run                  (src/tcl/tclmain.cc:534)
     [ 9] tcl::event_loop::run                      (src/tcl/tclmain.cc:619)
     [10] main                                (src/grsh/grshAppInit.cc:234)

Note that this "stack" is built up using various DOTRACE statements placed through the code, so it is NOT identical to the execution stack that might be reported by e.g. a debugger, since certain functions do not have DOTRACE statements and hence would not show up in the "bt" stack, while other functions that might have been inlined by the compiler would not show up in a debugger backtrace but would still be visible in "bt".

Now, looking at the backtrace above, we can see several things:

         return nub::soft_ref<nub::object>(nub::obj_factory::instance().new_checked_object(type));

So, clearly to get to the real bottom of things, one would have to dig a bit deeper; nevertheless "bt" gets you close.

         pkg->def( "new", "typename", &objNew, SRC_POS );

         [ 3] tcl::MainImpl::execCommand          (src/tcl/tclmain.cc:397)
         [ 4] tcl::MainImpl::handleLine           (src/tcl/tclmain.cc:345)
         [ 5] tcl::MainImpl::readlineLineComplete (src/tcl/tclmain.cc:298)
         [ 6] tcl::MainImpl::grabInput            (src/tcl/tclmain.cc:318)
         [ 7] tcl::MainImpl::stdinProc            (src/tcl/tclmain.cc:521)
         [ 8] tcl::MainImpl::run                  (src/tcl/tclmain.cc:534)

reflect that tcl has found some input available on stdin (stdinProc and grabInput), has found that it has a complete line of input (readlineLineComplete), has found that this complete line is also a complete tcl command (handleLine), and has thus executed this command (execCommand).

Just one more quick example of "bt"... suppose we're trying to get the eyeHeight of the Face object stored in variable $a, but we accidentally type "Face::eyeHeight a" instead of "FaceeyeHeight $a"; then we get the following backtrace:

     groovx>>> Face::eyeHeight blah
     Face::eyeHeight: expected long value but got "blah" (rutz::error)
     groovx 5>>> bt
     [ 0] tcl::convert_to(long*)                 (src/tcl/tclconvert.cc:143)
     [ 1] tcl::convert_to(unsigned long*)        (src/tcl/tclconvert.cc:159)
     [ 2] tcl::VecDispatcher::dispatch        (src/tcl/tclveccmd.cc:157)
     [ 3] tcl/Face::eyeHeight                 (src/visx/facetcl.cc:65)
     [ 4] tcl::command_group::rawInvoke        (src/tcl/tclcommandgroup.cc:285)
     [ 5] tcl::MainImpl::execCommand          (src/tcl/tclmain.cc:397)
     [ 6] tcl::MainImpl::handleLine           (src/tcl/tclmain.cc:345)
     [ 7] tcl::MainImpl::readlineLineComplete (src/tcl/tclmain.cc:298)
     [ 8] tcl::MainImpl::grabInput            (src/tcl/tclmain.cc:318)
     [ 9] tcl::MainImpl::stdinProc            (src/tcl/tclmain.cc:521)
     [10] tcl::MainImpl::run                  (src/tcl/tclmain.cc:534)
     [11] tcl::event_loop::run                      (src/tcl/tclmain.cc:619)
     [12] main                                (src/grsh/grshAppInit.cc:234)

Again, the line starting with "tcl/" shows the current entry point from the script side back into c++ (in this case the Face::eyeHeight command), and, from the contents of the stack above that line, we can see how the problem came from trying to convert the string "a" into an unsigned long value.

So, "bt" can be used to help tracking down where errors are occuring.

The other way to get pointers to specific locations in the c++ source is by looking up the usages associated with groovx script commands (either by using "?", or by giving the wrong number of arguments on purpose in order to force a usage warning):

     groovx>>> ? IO::loadGVX
             ::IO::loadGVX filename
             (defined at src/io/iotcl.cc:67)

which points us to this:

     pkg->def( "loadGVX", "filename", IO::loadGVX, SRC_POS );

or another example:

     groovx>>> Face::eyeHeight
     wrong # args: should be one of:
             "Face::eyeHeight objref(s)"
             "Face::eyeHeight objref(s) new_val(s)"
     (resolves to ::Face::eyeHeight, defined at src/visx/facetcl.cc:65)

which points us to this:

     tcl::defFieldContainer<Face>(pkg, SRC_POS);


7. The C++/Tcl bridge

Here are some examples of the C++/Tcl bridge.

Example 1 (from src/tcl/dlisttcl.cc)

   #include "tcl/tcllistobj.h"
   #include "tcl/tclpkg.h"

   namespace Dlist
   {
     tcl::list choose(tcl::list source_list, tcl::list index_list)
     {
       tcl::list result;

       for (unsigned int i = 0; i < index_list.length(); ++i)
         {
           unsigned int index = index_list.get<unsigned int>(i);

           result.append(source_list.at(index));
         }

       ASSERT(result.length() == index_list.length());

       return result;
     }
   }

Here we see how the C++ class tcl::list wraps the functionality of Tcl list objects, making them easily manipulable within C++. Next, we expose this choose() function as a Tcl command, [dlist::choose]:

   extern "C"
   int Dlist_Init(Tcl_Interp* interp)
   {
     PKG_CREATE(interp, "dlist", "$Revision: 10065 $");

     pkg->def( "choose", "source_list index_list", &Dlist::choose, SRC_POS );

     PKG_RETURN;
   }

  1. We have created a tcl::pkg with the PKG_CREATE() macro. By default packages are associated with a namespace, which we've specified here as "dlist".

  2. Next, we use pkg->def() to create the [dlist::choose] command. Commands go into the package's namespace by default, unless there is an explicit namespace qualifier in the given command name -- here we've just given "choose" as the command name (the first argument to def()).

  3. The second argument to def() gives a human-readable description of the arguments that the command expects. This description is fed back to the user in case he or she enters the command with the wrong number of arguments.

  4. The third argument to def() gives a pointer to the C++ function that we're trying to wrap, in this case &Dlist::choose. This parameter can be either a pointer to a free function, or a pointer to a member function, or a suitable functor object. Here's what tcl::pkg::def() does, in more detail.

    • tcl::pkg::def() (defined in src/tcl/tclpkg.h) calls tcl::make_command().

    • tcl::make_command() (defined in src/tcl/tclfunctor.h) calls tcl::makeGenericCmd (also defined in src/tcl/tclfunctor.h)

    • tcl::makeGenericCmd builds a tcl::Command using a suitable tcl::GenericCallback. The tcl::GenericCallback object is responsible for converting all of the commands arguments from Tcl to appropriate C++ types, and for converting the C++ result back to an appropriate Tcl object.

    • The C++/Tcl conversion routines are defined in src/tcl/tclconvert.h as a set of tcl::convert_to() and tcl::convert_from() overloads. That file contains overloads for the basic built-in types as well as for fundamental value types (like strings). This mechanism is extensible; users can simply provide additional tcl::convert_from() and tcl::convert_to() overloads for their own types, as long as these overloads are defined before they are needed by any tcl::pkg::def() call.

    • The tcl::Command object (defined in src/tcl/tclcmd.h) built by tcl::makeGenericCmd sets up the raw C callback with the Tcl interpreter. It does this through one final indirection, namely tcl::command_group (defined in src/tcl/tclcommandgroup.h).

    • tcl::command_group is responsible for managing an overload set of commands. Each tcl::command_group represents a single Tcl_CreateObjCommand() call. But, when the tcl::command_group gets called back by the Tcl interpreter, that command_group will look into its set of overloads to try to find whose number of arguments matches the number of arguments in the current command invocation. If one is found, then it is invoked; if no matching overload is found, then an error message is returnd via the Tcl interpreter.


The software described here is Copyright (c) 1998-2005, Rob Peters.
This page was generated Wed Dec 3 06:53:56 2008 by Doxygen version 1.5.5.